Topics › Dynamic Programming › DP on LIS
Largest Divisible Subset
LeetCode ↗ Java source ↗ Edit this article ↗
Jump to code:
Problem
Given a set of distinct positive integers nums, return a largest subset in which every pair (x, y) satisfies x % y == 0 or y % x == 0; that is, of any two chosen numbers, one divides the other. If several subsets have the maximum size, any of them is accepted.
Constraints: 1 ≤ n ≤ 1000, 1 ≤ nums[i] ≤ 2 × 10^9, all values distinct. The Java file returns an empty list for an empty array and returns the subset in ascending order.
Example 1
nums = [1, 2, 3]
answer = [1, 2] // [1, 3] is equally valid; 2 and 3 cannot both be chosen
Example 2
nums = [1, 16, 7, 8, 4]
answer = [1, 4, 8, 16]
Although the problem says “subset”, it is secretly a subsequence problem. Once the numbers are sorted, a divisible subset is a chain in which each number divides the next, which is the Longest Increasing Subsequence with a different comparison.
The key observation
Sort the values. Take any divisible subset and list it in ascending order: c1 < c2 < ... < cm. Every pair is divisible, so in particular each c_t divides c_{t+1}. Conversely, if each element divides the next, then by transitivity of divisibility (a | b and b | c imply a | c) every earlier element divides every later one, so the whole set is pairwise divisible.
So “pairwise divisible” collapses to a condition on neighbours only. That is the same shape as LIS, where “increasing” is checked between neighbours and transitivity of < takes care of the rest. Sorting fixes the order, so the smaller number of each pair always comes first and we only test larger % smaller == 0.
Approach 1: Brute force
Intuition. Try every subset of the sorted array (as a bitmask), check the neighbour condition on the chosen elements, and keep the largest valid one. Skipping masks that are not larger than the current best saves time but does not change the exponential bound.
Algorithm.
- Sort a copy of the array.
- For each mask from
1to2^n - 1with more bits than the best so far, walk its chosen indices in order and check that each chosen value divides the next chosen value. - Return the values of the best mask.
static List<Integer> bruteForce(int[] nums) {
int[] a = nums.clone();
Arrays.sort(a);
int n = a.length, bestMask = 0;
for (int mask = 1; mask < (1 << n); mask++) {
if (Integer.bitCount(mask) <= Integer.bitCount(bestMask)) continue;
int prev = -1;
boolean ok = true;
for (int i = 0; i < n && ok; i++) {
if ((mask >> i & 1) == 0) continue;
if (prev != -1 && a[i] % a[prev] != 0) ok = false; // consecutive chosen values must divide
prev = i;
}
if (ok) bestMask = mask;
}
List<Integer> res = new ArrayList<>();
for (int i = 0; i < n; i++) if ((bestMask >> i & 1) == 1) res.add(a[i]);
return res;
}
Complexity. Time O(2^n · n), space O(n). Usable only for tiny n; the tests use it as a reference.
Approach 2: Optimal, LIS-style DP with parent pointers
Intuition. After sorting, let dp[i] be the size of the largest divisible chain that ends at a[i]. Such a chain is either a[i] alone, or a chain ending at some earlier a[j] that divides a[i], extended by a[i]. We do not need to check a[i] against every member of that chain: all of them divide a[j], and a[j] divides a[i], so all of them divide a[i]. Therefore
dp[i] = 1 + max(dp[j]) over j < i with a[i] % a[j] == 0 (or 1 if no such j)
To output the subset itself, store parent[i] = j for the j that gave the maximum (and parent[i] = i for a chain that starts at i). Track last, an index with the largest dp, and follow the parents back.
Algorithm.
- Sort a copy
aof the input. - For each
i:dp[i] = 1,parent[i] = i; for eachj < iwitha[i] % a[j] == 0anddp[j] + 1 > dp[i], setdp[i] = dp[j] + 1andparent[i] = j. Updatelastifdp[i]is a new maximum. - Starting at
last, collecta[last]and move toparent[last]until an index is its own parent. Reverse the list.
static List<Integer> optimal(int[] nums) {
int n = nums.length;
List<Integer> res = new ArrayList<>();
if (n == 0) return res;
int[] a = nums.clone();
Arrays.sort(a);
int[] dp = new int[n], parent = new int[n]; // dp[i] = largest chain ending at a[i]
int last = 0;
for (int i = 0; i < n; i++) {
dp[i] = 1;
parent[i] = i;
for (int j = 0; j < i; j++) {
if (a[i] % a[j] == 0 && dp[j] + 1 > dp[i]) {
dp[i] = dp[j] + 1;
parent[i] = j;
}
}
if (dp[i] > dp[last]) last = i;
}
res.add(a[last]);
while (parent[last] != last) {
last = parent[last];
res.add(a[last]);
}
Collections.reverse(res);
return res;
}
Dry run on nums = [1, 16, 7, 8, 4], sorted to a = [1, 4, 7, 8, 16]:
| i | a[i] | earlier divisors (dp) | dp[i] | parent[i] |
|---|---|---|---|---|
| 0 | 1 | none | 1 | 0 |
| 1 | 4 | 1 (1) | 2 | 0 |
| 2 | 7 | 1 (1) | 2 | 0 |
| 3 | 8 | 1 (1), 4 (2) | 3 | 1 |
| 4 | 16 | 1 (1), 4 (2), 8 (3) | 4 | 3 |
last = 4; the walk visits 16 → 8 → 4 → 1, reversed to [1, 4, 8, 16].
Complexity. Time O(n^2) for the double loop plus O(n log n) for sorting. Space O(n) for dp, parent and the sorted copy.
Why sorting is required. Without it, a chain could need a later element to divide an earlier one, and the “extend a chain ending at j < i” step would miss it. For [4, 2, 8], unsorted processing would compare 2 only with 4, see that 4 does not divide 2, and never discover the chain 2, 4, 8 in the right order. Sorting puts every chain in ascending order, which is also the order of indices.
Edge cases and pitfalls
- Unsorted input. Always sort first, and sort a copy if the caller’s array must not change.
- Direction of the modulo. With ascending order, test
a[i] % a[j] == 0(the later, larger value divided by the earlier one). Reversing it only accepts equal values. - The value 1. It divides everything, so if present it can always be added to the chain; the DP finds this automatically.
- No two values divide each other. For primes such as
[2, 3, 5, 7]the answer is any single element. - Multiple correct answers.
[1, 2, 3]accepts[1, 2]or[1, 3]. The tests therefore check the size, that every value comes from the input, and that every pair divides, instead of comparing to one fixed list. - Empty input. Return an empty list before touching
dp[0]. - Large values. Values up to
2 × 10^9fit inint, and%never overflows, so nolongis needed.
Related problems
- Longest Increasing Subsequence is the same DP with
<instead of “divides”. - Print Longest Increasing Subsequence uses the identical parent-pointer reconstruction.
- Longest String Chain is another LIS variant that needs sorting before the DP.
- Longest Bitonic Subsequence is a further variant of the ending-at DP.