368. Largest Divisible Subset
Problem Description
Given a non-empty set of nums
containing distinct positive integers, the task is to find the largest subset of these integers such that each pair of numbers in the subset, (answer[i], answer[j])
, is in a relationship where one of the numbers is a multiple of the other. This means for any two numbers in the subset, either one will be divisible by the other without leaving a remainder. The subset containing the highest number of such numbers needs to be identified. If more than one subset fits this requirement, any of them may be returned as the solution.
Intuition
To solve this problem, we need an efficient way to determine relationships between numbers in terms of divisibility, and we want to build the largest group (subset) of numbers where each pair of numbers meets the divisibility condition.
The first insight is that if we sort the list of numbers nums
in ascending order, any larger number can only be divisible by a smaller number and not vice versa. This imposes an order on potential divisibility relationships.
The second insight is that we can use Dynamic Programming to build up knowledge of the largest divisible subset up to any given index as we iterate through our sorted list. We define an array f[i]
to represent the size of the largest divisible subset that includes nums[i]
as the largest number in the subset. So for each number nums[i]
, we look back at all previous numbers nums[j]
where j < i
and update f[i]
if nums[i] % nums[j] == 0
.
Lastly, knowing the size of the largest subset isn't enough; we want the subset itself. We keep track of the index k
at which we attain the maximum size of the subset. Once we finish populating our f
array, we can backtrack from nums[k]
and construct the actual subset by looking at elements that could be used to reach nums[k]
. To construct the result, we traverse in reverse, starting from nums[k]
going backward, and add numbers to our subset ans
that have the correct divisibility property and help us reach the previously calculated optimum size m
at each step until we've constructed the full largest subset.
Learn more about Math, Dynamic Programming and Sorting patterns.
Solution Approach
The solution implements Dynamic Programming, which is a method to solve problems by combining the solutions of subproblems.
Let's walk through the code and explain the solution's approach:
- First, the list
nums
is sorted so we can guarantee that for any pair(nums[i], nums[j])
wherei > j
,nums[i]
can only be divisible bynums[j]
and not vice versa.
nums.sort()
- The variable
n
is initialized to the length ofnums
, and an arrayf
of the same length is created with all elements set to1
. This array will hold the size of the largest divisible subset ending withnums[i]
.
n = len(nums)
f = [1] * n
- Two variables,
k
andm
, are used.k
is the index at which the largest subset ends, andm
will hold the size of the largest subset.
k = 0
- We iterate through each element
nums[i]
from start to end, and for eachi
, we iterate backwards fromi - 1
to0
to check all possiblenums[j]
thatnums[i]
could be divisible by. Ifnums[i] % nums[j]
is0
, meaningnums[j]
dividesnums[i]
without a remainder, we updatef[i]
if it would lead to a larger subset size than currently recorded atf[i]
.
for i in range(n):
for j in range(i):
if nums[i] % nums[j] == 0:
f[i] = max(f[i], f[j] + 1)
- While updating
f[i]
, we also keep track of the maximum subset size we've seen so far and the corresponding indexk
.
if f[k] < f[i]: k = i
- Now the variable
m
is assigned to the length of the largest subset.
m = f[k]
- To re-construct the actual subset, we initialize an empty list
ans
and loop backwards from the indexk
of the largest number belonging to the largest divisible subset. We decrementm
each time we add a new element toans
. For each element we consider, it must satisfy two conditions:nums[k] % nums[i] == 0
(divisibility) andf[i] == m
(it contributes to the maximum subset size).
i = k ans = [] while m: if nums[k] % nums[i] == 0 and f[i] == m: ans.append(nums[i]) k, m = i, m - 1 i -= 1
- Finally,
ans
is returned, which contains the elements ofnums
forming the largest divisible subset.
return ans
The use of sorting, coupled with dynamic programming and some clever backtracking, enables this solution to find the largest subset efficiently. The Dynamic Programming pattern here helps avoid redundant re-computation by storing optimal sub-solutions in the array f
.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with an example. Consider the following small set of distinct positive integers: nums = [1, 2, 4, 8]
.
-
Sort the array
nums
. In this example,nums
is already sorted in ascending order:[1, 2, 4, 8]
. -
Initialize
n = len(nums)
which is4
in our case, andf = [1, 1, 1, 1]
, corresponding to the fact that the largest divisible subset including each number individually is just the number itself. -
Initialize
k = 0
to keep track of the index of the largest subset, although its value will likely change as we iterate through the array. -
Iterate through each element
nums[i]
. We check all previous elementsnums[j]
to find ifnums[i]
can be made larger by includingnums[j]
. As1
divides every other integer,f
will become[1, 2, 3, 4]
by the end. -
During iteration, we keep track of the maximum subset size and the index
k
. After completion,k = 3
(corresponding to number8
) and the maximum subset sizem = f[k] = 4
. -
To reconstruct the subset, we start with
ans = []
and loop backwards fromk = 3
. As we checknums[i]
we look for divisibility (nums[k] % nums[i] == 0
) and iff[i]
equalsm
. We find that all elements innums
satisfy this, so sequentially,ans
becomes[8]
,[8, 4]
,[8, 4, 2]
, and finally[8, 4, 2, 1]
. -
The resulting subset, which is the largest one where every pair of elements are divisible by one another, is
[8, 4, 2, 1]
.
By following these steps, the solution capitalizes on the sorted property of the array and the previously computed subset sizes, effectively avoiding redundant calculations and leading to a more efficient algorithm.
Solution Implementation
1from typing import List
2
3class Solution:
4 def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
5 # Sort the array to ensure divisibility can be checked in ascending order
6 nums.sort()
7 n = len(nums)
8 # Initialize a list to keep track of the longest subset ending at each index
9 dp = [1] * n
10 max_index = 0 # To track the index at which the largest divisible subset ends
11
12 # Build up the dp array with the length of each longest divisible subset
13 for i in range(n):
14 for j in range(i):
15 # Check if the current number is divisible by the j-th number
16 if nums[i] % nums[j] == 0:
17 # Update the dp[i] to the maximum length achievable
18 dp[i] = max(dp[i], dp[j] + 1)
19 # Update the index of the largest divisible subset if a new max is found
20 if dp[max_index] < dp[i]:
21 max_index = i
22
23 # The size of the largest divisible subset
24 subset_size = dp[max_index]
25 # Start from the end element of the largest subset
26 current_index = max_index
27 # List to store the largest divisible subset
28 largest_subset = []
29
30 # Backtrack from the largest index found to build the subset
31 while subset_size:
32 # If the current number divides the number at max_index and its dp value
33 # corresponds to the current subset size, add it to the result
34 if nums[max_index] % nums[current_index] == 0 and dp[current_index] == subset_size:
35 largest_subset.append(nums[current_index])
36 # Update the max_index and decrement the size for next numbers
37 max_index, subset_size = current_index, subset_size - 1
38 # Move one index backwards
39 current_index -= 1
40
41 # Return the largest divisible subset in the original order
42 return largest_subset
43
1import java.util.Arrays;
2import java.util.ArrayList;
3import java.util.List;
4
5class Solution {
6
7 /**
8 * Finds the largest divisible subset in the given array.
9 *
10 * @param nums The array of numbers
11 * @return The largest divisible subset
12 */
13 public List<Integer> largestDivisibleSubset(int[] nums) {
14 // Sort the array of numbers
15 Arrays.sort(nums);
16
17 // Length of the nums array
18 int length = nums.length;
19
20 // Array to store the size of the largest divisible subset
21 // that ends with nums[i]
22 int[] dp = new int[length];
23
24 // Initialize all values in dp to 1
25 Arrays.fill(dp, 1);
26
27 // Variable to track the index of the largest element of the subset
28 int maxIndex = 0;
29
30 // Dynamic programming to fill the dp array
31 for (int i = 0; i < length; ++i) {
32 for (int j = 0; j < i; ++j) {
33 // If nums[i] is divisible by nums[j], update dp[i]
34 if (nums[i] % nums[j] == 0) {
35 dp[i] = Math.max(dp[i], dp[j] + 1);
36 }
37 }
38
39 // Update maxIndex if a larger subset is found
40 if (dp[maxIndex] < dp[i]) {
41 maxIndex = i;
42 }
43 }
44
45 // Size of the largest divisible subset
46 int subsetSize = dp[maxIndex];
47
48 // List to store the largest divisible subset
49 List<Integer> result = new ArrayList<>();
50
51 // Construct the result list by going backwards from maxIndex
52 for (int i = maxIndex; subsetSize > 0; --i) {
53 // If the current number can be included in the subset
54 if (nums[maxIndex] % nums[i] == 0 && dp[i] == subsetSize) {
55 result.add(nums[i]);
56 maxIndex = i; // Update the maxIndex to the current number's index
57 --subsetSize; // Decrease the size of the subset as we add to the result
58 }
59 }
60
61 return result;
62 }
63}
64
1class Solution {
2public:
3 vector<int> largestDivisibleSubset(vector<int>& nums) {
4 // Sort the input array to make the divisibility condition easier to check.
5 sort(nums.begin(), nums.end());
6
7 // Get the size of the array.
8 int n = nums.size();
9 // Array to store the size of the largest divisible subset that ends with nums[i].
10 vector<int> subsetSizes(n, 1);
11
12 // Variable to keep track of the index at which the largest subset ends.
13 int maxSubsetIndex = 0;
14
15 // Calculate the size of the largest subset where each element is divisible by its previous ones.
16 for (int i = 0; i < n; ++i) {
17 for (int j = 0; j < i; ++j) {
18 if (nums[i] % nums[j] == 0) {
19 // If nums[i] is divisible by nums[j], consider this as a potential maximum size.
20 subsetSizes[i] = max(subsetSizes[i], subsetSizes[j] + 1);
21 }
22 }
23 // Update the index of the largest subset if the current one is larger.
24 if (subsetSizes[maxSubsetIndex] < subsetSizes[i]) {
25 maxSubsetIndex = i;
26 }
27 }
28
29 // Construct the largest subset by iterating from the end to the beginning of the subset.
30 int currentSize = subsetSizes[maxSubsetIndex];
31 vector<int> largestSubset;
32 for (int i = maxSubsetIndex; currentSize > 0; --i) {
33 // If nums[i] is part of the subset, add it to the result.
34 if (nums[maxSubsetIndex] % nums[i] == 0 && subsetSizes[i] == currentSize) {
35 largestSubset.push_back(nums[i]);
36 // Update the index to the last added number and decrement the currentSize.
37 maxSubsetIndex = i;
38 --currentSize;
39 }
40 }
41 // Return the constructed largest divisible subset.
42 return largestSubset;
43 }
44};
45
1// Function to sort an array of numbers in ascending order.
2const sort = (array: number[]): number[] => array.sort((a, b) => a - b);
3
4// Function to retrieve the maximum of two numbers.
5const max = (a: number, b: number): number => (a > b ? a : b);
6
7// Function to get the largest divisible subset from an array of numbers.
8function largestDivisibleSubset(nums: number[]): number[] {
9 // Sort the input array to make the divisibility condition easier to check.
10 nums = sort(nums);
11
12 // Get the size of the array.
13 const n: number = nums.length;
14 // Array to store the size of the largest divisible subset that ends with nums[i].
15 const subsetSizes: number[] = new Array(n).fill(1);
16
17 // Variable to keep track of the index at which the largest subset ends.
18 let maxSubsetIndex: number = 0;
19
20 // Calculate the size of the largest subset where each element is divisible by its previous ones.
21 for (let i = 0; i < n; ++i) {
22 for (let j = 0; j < i; ++j) {
23 if (nums[i] % nums[j] === 0) {
24 // If nums[i] is divisible by nums[j], consider this as a potential maximum size.
25 subsetSizes[i] = max(subsetSizes[i], subsetSizes[j] + 1);
26 }
27 }
28 // Update the index of the largest subset if the current one is larger.
29 if (subsetSizes[maxSubsetIndex] < subsetSizes[i]) {
30 maxSubsetIndex = i;
31 }
32 }
33
34 // Construct the largest subset by iterating from the end to the beginning of the subset.
35 let currentSize: number = subsetSizes[maxSubsetIndex];
36 const largestSubset: number[] = [];
37
38 for (let i = maxSubsetIndex; currentSize > 0; --i) {
39 // If nums[i] is part of the subset, add it to the result.
40 if (nums[maxSubsetIndex] % nums[i] === 0 && subsetSizes[i] === currentSize) {
41 largestSubset.push(nums[i]);
42 // Update the index to the last added number and decrement the currentSize.
43 maxSubsetIndex = i;
44 --currentSize;
45 }
46 }
47 // Return the constructed largest divisible subset in reverse to maintain original order.
48 return largestSubset.reverse();
49}
50
Time and Space Complexity
Time Complexity
The time complexity of the code is determined by the nested loops and the operations within them.
- The outer loop runs
n
times wheren
is the number of elements innums
. - The inner loop, for each element of the outer loop, runs a maximum of
i
times which is less thann
. - Therefore, in total, it has a complexity of approximately O(n^2) due to the double-loop structure.
The while loop at the end of the code runs a maximum of n
times corresponding to the size of the nums
list. The operations inside are of constant time. However, this does not affect the overall time complexity since it's linear and the dominant factor is the O(n^2) from the nested loops. Hence the overall time complexity remains O(n^2).
Space Complexity
The space complexity of the code is determined by the additional storage used:
- The
f
list which is of sizen
contributes O(n) to the space complexity. - The
ans
list could potentially reach the same size asnums
in the case that all elements are multiples of each other. Thus, it also contributes O(n) to the space complexity.
The variables k
, i
, and m
use constant space.
Therefore, the overall space complexity is O(n) as it relies on the storage required for the f
and ans
lists, which scale linearly with the size of the input nums
.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Don’t Miss This!