Facebook Pixel

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]), or
  • answer[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:

  1. Sort the array first to ensure smaller numbers come before larger ones
  2. Use f[i] to track the length of the largest divisible subset ending at index i
  3. For each element at position i, check all previous elements j where j < i
  4. If nums[i] % nums[j] == 0, then nums[i] can extend the subset ending at j
  5. Track the index k where the longest subset ends
  6. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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 by nums[j], then nums[i] can extend whatever chain ended at j
  • 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 to i (new reference point) and decrement m (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 Evaluator

Example 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 âś“, so f[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 âś“, so f[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 âś“, so f[3] = max(1, f[0] + 1) = 2
  • Check j = 1: 4 % 2 == 0 âś“, so f[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 âś“, so f[4] = max(1, f[0] + 1) = 2
  • Check j = 1: 8 % 2 == 0 âś“, so f[4] = max(2, f[1] + 1) = 3
  • Check j = 2: 8 % 3 == 2 âś—, no update
  • Check j = 3: 8 % 4 == 0 âś“, so f[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 âś“ and f[4] == 4 âś“ → Add 8, update k = 4, m = 3
  • i = 3: 8 % 4 == 0 âś“ and f[3] == 3 âś“ → Add 4, update k = 3, m = 2
  • i = 2: 4 % 3 == 1 âś— → Skip
  • i = 1: 4 % 2 == 0 âś“ and f[1] == 2 âś“ → Add 2, update k = 1, m = 1
  • i = 0: 2 % 1 == 0 âś“ and f[0] == 1 âś“ → Add 1, update k = 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²)
  • The reconstruction phase (while loop):
    • In the worst case, iterates through all elements once, taking O(n) time

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 size n to store the length of longest divisible subset ending at each index: O(n)
  • The output array ans which can contain at most n 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the relationship between a tree and a graph?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More