368. Largest Divisible Subset
Problem Description
You are given a set of distinct positive integers nums
. Your task is to find the largest subset where any two elements in the subset are divisible by each other.
Specifically, for any pair of elements (answer[i], answer[j])
in your subset, one of these conditions must be true:
answer[i] % answer[j] == 0
(answer[i] is divisible by answer[j]), oranswer[j] % answer[i] == 0
(answer[j] is divisible by answer[i])
The goal is to return the subset with the maximum number of elements that satisfies this divisibility property. If multiple valid subsets of the same maximum size exist, you can return any one of them.
For example, if nums = [1, 2, 4, 8]
, the entire array forms a valid subset because:
- 2 is divisible by 1
- 4 is divisible by 2 and 1
- 8 is divisible by 4, 2, and 1
The solution uses dynamic programming with the following approach:
- Sort the array first to ensure smaller numbers come before larger ones
- Use
f[i]
to track the length of the largest divisible subset ending at indexi
- For each element at position
i
, check all previous elementsj
wherej < i
- If
nums[i] % nums[j] == 0
, thennums[i]
can extend the subset ending atj
- Track the index
k
where the longest subset ends - Reconstruct the actual subset by backtracking from position
k
The time complexity is O(n²)
where n
is the length of the input array, due to the nested loops checking divisibility relationships.
Intuition
The key insight is recognizing that if we have a valid divisible subset, and we sort it, the elements form a chain where each element divides the next one. For instance, [1, 2, 4, 8]
forms a chain where 1|2
, 2|4
, and 4|8
(using |
to denote "divides").
This observation leads us to think about the problem differently. If we sort the entire input array first, then for any valid subset, its elements will maintain a divisibility chain property when taken in sorted order.
Why is this helpful? Because it transforms our problem into finding the longest chain where each element divides the next. This is similar to finding the Longest Increasing Subsequence (LIS), but instead of comparing if nums[i] > nums[j]
, we check if nums[i] % nums[j] == 0
.
The dynamic programming approach emerges naturally from this insight:
- For each position
i
, we ask: "What's the longest divisible chain that ends at this element?" - To answer this, we look at all previous positions
j < i
- If
nums[i]
is divisible bynums[j]
, thennums[i]
can extend whatever chain ended atj
- We take the maximum among all such possibilities
The beauty of sorting first is that it guarantees we only need to check if the larger number is divisible by the smaller one (not both directions), simplifying our logic. Additionally, when we build a chain like [2, 4, 8]
, we know that 8 is divisible by 2 (not just by 4) due to the transitive property of divisibility - if 8 % 4 == 0
and 4 % 2 == 0
, then 8 % 2 == 0
.
This reduction to a chain-finding problem with dynamic programming gives us an efficient O(n²)
solution.
Solution Approach
The implementation follows a dynamic programming approach with backtracking to reconstruct the solution:
Step 1: Sort the array
nums.sort()
Sorting ensures that for any valid divisible subset, elements appear in increasing order, simplifying our divisibility checks.
Step 2: Initialize DP array
f = [1] * n
Create array f
where f[i]
represents the length of the longest divisible subset ending at index i
. Initialize all values to 1 since each element forms a valid subset by itself.
Step 3: Build DP table
for i in range(n):
for j in range(i):
if nums[i] % nums[j] == 0:
f[i] = max(f[i], f[j] + 1)
For each position i
, check all previous positions j
. If nums[i]
is divisible by nums[j]
, then nums[i]
can extend the subset ending at j
. Update f[i]
to be the maximum of its current value or f[j] + 1
.
Step 4: Track the best ending position
if f[k] < f[i]: k = i
Variable k
keeps track of the index where the longest divisible subset ends. This helps us know where to start our backtracking.
Step 5: Reconstruct the subset
m = f[k] 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
Starting from position k
with subset length m
, work backwards:
- Check if
nums[k] % nums[i] == 0
(ensuring divisibility) - Check if
f[i] == m
(ensuring this element belongs to our optimal chain) - If both conditions are met, add
nums[i]
to our answer - Update
k
toi
(new reference point) and decrementm
(looking for the next element in the chain) - Continue moving backwards with
i -= 1
The reconstruction works because we're essentially tracing back the decisions made during the DP construction, picking elements that form the longest valid chain ending at position k
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with nums = [1, 2, 3, 4, 8]
.
Step 1: Sort the array
Array is already sorted: [1, 2, 3, 4, 8]
Step 2: Initialize DP array
f = [1, 1, 1, 1, 1]
(each element forms a subset by itself)
Step 3: Build DP table
For i = 0
(nums[0] = 1): No previous elements, so f[0] = 1
For i = 1
(nums[1] = 2):
- Check
j = 0
:2 % 1 == 0
âś“, sof[1] = max(1, f[0] + 1) = 2
- Result:
f = [1, 2, 1, 1, 1]
For i = 2
(nums[2] = 3):
- Check
j = 0
:3 % 1 == 0
âś“, sof[2] = max(1, f[0] + 1) = 2
- Check
j = 1
:3 % 2 == 1
âś—, no update - Result:
f = [1, 2, 2, 1, 1]
For i = 3
(nums[3] = 4):
- Check
j = 0
:4 % 1 == 0
âś“, sof[3] = max(1, f[0] + 1) = 2
- Check
j = 1
:4 % 2 == 0
âś“, sof[3] = max(2, f[1] + 1) = 3
- Check
j = 2
:4 % 3 == 1
âś—, no update - Result:
f = [1, 2, 2, 3, 1]
For i = 4
(nums[4] = 8):
- Check
j = 0
:8 % 1 == 0
âś“, sof[4] = max(1, f[0] + 1) = 2
- Check
j = 1
:8 % 2 == 0
âś“, sof[4] = max(2, f[1] + 1) = 3
- Check
j = 2
:8 % 3 == 2
âś—, no update - Check
j = 3
:8 % 4 == 0
âś“, sof[4] = max(3, f[3] + 1) = 4
- Result:
f = [1, 2, 2, 3, 4]
Step 4: Find best ending position
Maximum value in f
is 4 at index k = 4
Step 5: Reconstruct the subset
Starting with k = 4
, m = 4
:
i = 4
:8 % 8 == 0
âś“ andf[4] == 4
✓ → Add 8, updatek = 4
,m = 3
i = 3
:8 % 4 == 0
âś“ andf[3] == 3
✓ → Add 4, updatek = 3
,m = 2
i = 2
:4 % 3 == 1
✗ → Skipi = 1
:4 % 2 == 0
âś“ andf[1] == 2
✓ → Add 2, updatek = 1
,m = 1
i = 0
:2 % 1 == 0
âś“ andf[0] == 1
✓ → Add 1, updatek = 0
,m = 0
Final answer: [8, 4, 2, 1]
(or reversed to [1, 2, 4, 8]
)
This forms a valid divisible subset where each pair satisfies the divisibility condition.
Solution Implementation
1class Solution:
2 def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
3 # Sort the array to ensure divisibility check works correctly
4 # If a divides b and b divides c, then a divides c (transitivity)
5 nums.sort()
6 n = len(nums)
7
8 # dp[i] represents the length of largest divisible subset ending at index i
9 dp = [1] * n
10
11 # Track the index with maximum subset length
12 max_length_index = 0
13
14 # Build dp array using dynamic programming
15 for i in range(n):
16 # Check all previous elements that could form a subset with nums[i]
17 for j in range(i):
18 # If nums[i] is divisible by nums[j], we can extend the subset ending at j
19 if nums[i] % nums[j] == 0:
20 dp[i] = max(dp[i], dp[j] + 1)
21
22 # Update the index of maximum length subset found so far
23 if dp[max_length_index] < dp[i]:
24 max_length_index = i
25
26 # Reconstruct the actual subset by backtracking
27 max_length = dp[max_length_index]
28 current_index = max_length_index
29 result = []
30
31 # Start from the end and work backwards to build the subset
32 i = max_length_index
33 while max_length > 0:
34 # Check if current element can be part of the subset
35 # Conditions: nums[current_index] is divisible by nums[i] and dp[i] matches expected length
36 if nums[current_index] % nums[i] == 0 and dp[i] == max_length:
37 result.append(nums[i])
38 current_index = i
39 max_length -= 1
40 i -= 1
41
42 return result
43
1class Solution {
2 public List<Integer> largestDivisibleSubset(int[] nums) {
3 // Sort the array to ensure smaller numbers come before larger ones
4 Arrays.sort(nums);
5
6 int n = nums.length;
7 // dp[i] represents the length of the largest divisible subset ending at index i
8 int[] dp = new int[n];
9 Arrays.fill(dp, 1); // Each element forms a subset of size 1 by itself
10
11 // Track the index with the maximum subset length
12 int maxIndex = 0;
13
14 // Build dp array using dynamic programming
15 for (int i = 0; i < n; i++) {
16 // Check all previous elements
17 for (int j = 0; j < i; j++) {
18 // If nums[i] is divisible by nums[j], we can extend the subset ending at j
19 if (nums[i] % nums[j] == 0) {
20 dp[i] = Math.max(dp[i], dp[j] + 1);
21 }
22 }
23 // Update the index of maximum subset length
24 if (dp[maxIndex] < dp[i]) {
25 maxIndex = i;
26 }
27 }
28
29 // Reconstruct the largest divisible subset
30 int maxLength = dp[maxIndex];
31 List<Integer> result = new ArrayList<>();
32
33 // Backtrack from maxIndex to build the result
34 for (int i = maxIndex; maxLength > 0; i--) {
35 // Check if current element can be part of the subset
36 // Conditions: nums[maxIndex] divisible by nums[i] AND dp[i] matches expected length
37 if (nums[maxIndex] % nums[i] == 0 && dp[i] == maxLength) {
38 result.add(nums[i]);
39 maxIndex = i; // Update maxIndex for next iteration
40 maxLength--; // Decrease expected length
41 }
42 }
43
44 return result;
45 }
46}
47
1class Solution {
2public:
3 vector<int> largestDivisibleSubset(vector<int>& nums) {
4 // Sort the array to ensure smaller elements come before larger ones
5 // This allows us to check divisibility in one direction only
6 sort(nums.begin(), nums.end());
7
8 int n = nums.size();
9
10 // dp[i] stores the size of the largest divisible subset ending at index i
11 vector<int> dp(n, 1);
12
13 // Track the index of the element with the largest subset
14 int maxIndex = 0;
15
16 // Build up the dp array
17 for (int i = 0; i < n; ++i) {
18 // Check all previous elements
19 for (int j = 0; j < i; ++j) {
20 // If nums[i] is divisible by nums[j], we can extend the subset ending at j
21 if (nums[i] % nums[j] == 0) {
22 dp[i] = max(dp[i], dp[j] + 1);
23 }
24 }
25
26 // Update the index of the maximum subset if current is larger
27 if (dp[maxIndex] < dp[i]) {
28 maxIndex = i;
29 }
30 }
31
32 // Reconstruct the largest divisible subset
33 int subsetSize = dp[maxIndex];
34 vector<int> result;
35
36 // Start from maxIndex and work backwards to build the subset
37 int currentIndex = maxIndex;
38 for (int i = maxIndex; subsetSize > 0; --i) {
39 // Check if current element can be part of the subset
40 // Conditions: nums[currentIndex] is divisible by nums[i] AND
41 // dp[i] matches the expected size for this position in the subset
42 if (nums[currentIndex] % nums[i] == 0 && dp[i] == subsetSize) {
43 result.push_back(nums[i]);
44 currentIndex = i;
45 --subsetSize;
46 }
47 }
48
49 return result;
50 }
51};
52
1/**
2 * Finds the largest subset where every pair of elements (a, b) satisfies either a % b = 0 or b % a = 0
3 * @param nums - Array of positive integers
4 * @returns The largest divisible subset
5 */
6function largestDivisibleSubset(nums: number[]): number[] {
7 // Sort the array in ascending order to ensure smaller elements come before larger ones
8 nums.sort((a, b) => a - b);
9
10 const arrayLength: number = nums.length;
11
12 // Dynamic programming array where dp[i] represents the size of largest subset ending at index i
13 const dp: number[] = Array(arrayLength).fill(1);
14
15 // Track the index of the element that forms the largest subset
16 let maxSubsetIndex: number = 0;
17
18 // Build up the dp array
19 for (let currentIndex = 0; currentIndex < arrayLength; ++currentIndex) {
20 // Check all previous elements that could form a subset with current element
21 for (let previousIndex = 0; previousIndex < currentIndex; ++previousIndex) {
22 // If current element is divisible by previous element, they can be in same subset
23 if (nums[currentIndex] % nums[previousIndex] === 0) {
24 dp[currentIndex] = Math.max(dp[currentIndex], dp[previousIndex] + 1);
25 }
26 }
27
28 // Update the index of maximum subset if current subset is larger
29 if (dp[maxSubsetIndex] < dp[currentIndex]) {
30 maxSubsetIndex = currentIndex;
31 }
32 }
33
34 // Reconstruct the actual subset from the dp array
35 let remainingElements: number = dp[maxSubsetIndex];
36 const result: number[] = [];
37
38 // Backtrack from the largest element to build the subset
39 for (let index = maxSubsetIndex; remainingElements > 0; --index) {
40 // Check if current element can be part of the subset chain
41 if (nums[maxSubsetIndex] % nums[index] === 0 && dp[index] === remainingElements) {
42 result.push(nums[index]);
43 maxSubsetIndex = index;
44 --remainingElements;
45 }
46 }
47
48 return result;
49}
50
Time and Space Complexity
Time Complexity: O(n²)
The algorithm consists of several parts:
- Sorting the array takes
O(n log n)
time - The nested loops for building the DP array
f
:- Outer loop runs
n
times - Inner loop runs up to
i
times for each iteration - Total iterations:
0 + 1 + 2 + ... + (n-1) = n(n-1)/2 = O(n²)
- Outer loop runs
- The reconstruction phase (while loop):
- In the worst case, iterates through all elements once, taking
O(n)
time
- In the worst case, iterates through all elements once, taking
Overall time complexity is dominated by the nested loops: O(n log n) + O(n²) + O(n) = O(n²)
Space Complexity: O(n)
The space usage includes:
- Array
f
of sizen
to store the length of longest divisible subset ending at each index:O(n)
- The output array
ans
which can contain at mostn
elements:O(n)
- A few auxiliary variables (
k
,m
,i
):O(1)
Total space complexity: O(n) + O(n) + O(1) = O(n)
Note: If we don't count the output array as extra space (as is common in complexity analysis), the space complexity would just be O(n)
for the DP array.
Common Pitfalls
Pitfall 1: Incorrect Divisibility Check Direction
Problem: A common mistake is checking nums[j] % nums[i] == 0
instead of nums[i] % nums[j] == 0
when building the DP array. Since the array is sorted in ascending order, we want to check if the larger number (nums[i]) is divisible by the smaller number (nums[j]).
Wrong approach:
if nums[j] % nums[i] == 0: # Incorrect!
dp[i] = max(dp[i], dp[j] + 1)
Correct approach:
if nums[i] % nums[j] == 0: # Correct
dp[i] = max(dp[i], dp[j] + 1)
Pitfall 2: Forgetting to Sort the Array
Problem: Without sorting, the DP approach breaks down because we rely on the transitive property of divisibility. If the array isn't sorted, we might miss valid subset extensions.
Example of failure without sorting:
nums = [4, 1, 2, 8] # Unsorted # Without sorting, when processing 2, we haven't seen 1 yet # This leads to incorrect subset construction
Solution: Always sort the array first:
nums.sort() # Essential step
Pitfall 3: Incorrect Reconstruction Logic
Problem: During backtracking, forgetting to check both conditions (nums[current_index] % nums[i] == 0
AND dp[i] == max_length
) can lead to including wrong elements in the subset.
Wrong reconstruction:
while max_length > 0: if dp[i] == max_length: # Missing divisibility check! result.append(nums[i]) max_length -= 1 i -= 1
Correct reconstruction:
while max_length > 0: if nums[current_index] % nums[i] == 0 and dp[i] == max_length: result.append(nums[i]) current_index = i # Update reference point max_length -= 1 i -= 1
Pitfall 4: Not Updating the Reference Point During Reconstruction
Problem: Forgetting to update current_index
when adding an element to the result causes incorrect divisibility checks in subsequent iterations.
Wrong approach:
if nums[current_index] % nums[i] == 0 and dp[i] == max_length: result.append(nums[i]) # Forgot to update current_index! max_length -= 1
Correct approach:
if nums[current_index] % nums[i] == 0 and dp[i] == max_length: result.append(nums[i]) current_index = i # Must update to maintain chain integrity max_length -= 1
Pitfall 5: Edge Case with Single Element
Problem: Not handling the case where the input array has only one element, though the provided solution handles this correctly by initializing dp = [1] * n
.
Prevention: Ensure the solution works for:
- Empty array (though typically problem constraints guarantee at least one element)
- Single element array:
[3]
should return[3]
- Arrays where no elements are divisible:
[2, 3, 5]
should return any single element
What's the relationship between a tree and a graph?
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!