LeetCode #368 Medium

Largest Divisible Subset

Given a set of distinct positive integers nums, return the largest subset such that for every pair (a, b) in the subset, either a % b == 0 or b % a == 0.

Constraints
  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 2 * 10⁹
  • All the integers in nums are unique.
dynamic-programmingmathsorting
Open on LeetCode ↗
02

Intuition

Largest divisible subset finds the biggest subset where every pair divides one another. Checking all pairs in a candidate subset sounds necessary, but a property of divisibility removes most of the work. Divisibility is transitive: if a divides b and b divides c, then a divides c. So in a sorted subset, checking each element against only its immediate predecessor is enough: - Sorting first means every element need only be tested against the largest element chosen so far, not against all of them. Without sorting, no such ordering exists and the pairwise property cannot be reduced. The structure is then exactly Longest Increasing Subsequence with divisibility replacing the comparison. Define dp[i] as the size of the largest valid subset ending at index i. For each i, look at every earlier j, and if nums[i] % nums[j] == 0, consider extending that subset. Reconstructing the subset — not just its size — needs a parent array recording which j gave each best value. Following the parent chain backwards from the largest dp value rebuilds the subset, then reverse it. That reconstruction is where solutions fail. Tracking only sizes answers a different question than the one asked, which requires the elements themselves. The maximum is not necessarily at the last index, so the whole dp array must be scanned to find where the chain starts. An empty input returns an empty list, and a single element is trivially valid since a number divides itself.

How to spot this pattern

The shape is: sorted array, find the longest chain where each element satisfies a transitive relation with the previous. LIS uses <, this uses %. Whenever a problem asks for the longest chain with a transitive property and you can sort to make the property directional, the LIS-like DP applies. Reconstruction with parent pointers is the standard way to recover the actual subsequence.

03

Approach

Try it first

Before reading on: price up what sorting first costs here, then ask what a single cell should mean, and which earlier cells it needs. Aim for O(n²) time and O(n) space.

1

Sort the array first

Sorting is what makes the pairwise condition tractable — without an ordering, every pair must be checked and no reduction is possible.

2

Exploit transitivity

If a divides b and b divides c, then a divides c. So each element need only be tested against the largest already chosen, not against all of them.

3

Define the LIS-style state

dp[i] is the size of the largest valid subset ending at index i. This is Longest Increasing Subsequence with divisibility replacing the comparison.

4

Extend on divisibility

For each i, scan every earlier j. When nums[i] % nums[j] == 0 and extending improves dp[i], take that extension.

5

Record parents for reconstruction

Store which j produced each best value. Tracking only sizes answers a different question — the problem requires the elements themselves.

6

Rebuild from the maximum

Scan the whole dp array for the largest value — it is not necessarily at the last index — then follow the parent chain back and reverse it.

7

Cost of the approach

Sorting is O(n log n) and the nested scan O(n²) time, which dominates. Space is O(n) for the dp and parent arrays.

04

Solution & live demo

▶1class Solution:
▶2 def largestDivisibleSubset(self, nums):
▶3 nums.sort()
▶4 n = len(nums)
▶5 dp = [1] * n
▶6 parent = [-1] * n
▶7 best_idx = 0
▶8 for i in range(1, n):
▶9 for j in range(i):
▶10 if nums[i] % nums[j] == 0 and dp[j] + 1 > dp[i]:
▶11 dp[i] = dp[j] + 1
▶12 parent[i] = j
▶13 if dp[i] > dp[best_idx]:
▶14 best_idx = i
▶15 result = []
▶16 idx = best_idx
▶17 while idx != -1:
▶18 result.append(nums[idx])
▶19 idx = parent[idx]
▶20 return result[::-1]
05

Common pitfalls

Checking nums[j] % nums[i] == 0 instead of nums[i] % nums[j] == 0

✗ Wrong
if nums[j] % nums[i] == 0:
✓ Right
if nums[i] % nums[j] == 0:

After sorting, nums[j] <= nums[i] for j < i. The condition should be 'the larger divides evenly by the smaller', which is nums[i] % nums[j] == 0. Reversing it checks if the smaller divides by the larger, which is almost never true.

Forgetting to sort the array before the DP

✗ Wrong
dp = [1] * len(nums)
for i in range(len(nums)):
✓ Right
nums.sort()
dp = [1] * len(nums)
for i in range(len(nums)):

Without sorting, a larger number might appear before a smaller one. The left-to-right scan would miss valid chains where the divisor appears later in the unsorted array.

Not tracking parent pointers for reconstruction

✗ Wrong
# only track dp[i], no parent
max_idx = dp.index(max(dp))
return [nums[max_idx]]
✓ Right
parent = [-1] * n
...
result = []
while idx != -1:
    result.append(nums[idx])
    idx = parent[idx]

The problem asks for the actual subset, not just its size. Without parent pointers, you can only return the last element of the chain, not the entire chain.

06

Edge cases

All elements are powers of 2, e.g. [1, 2, 4, 8]

Every element divides every later one. The entire sorted array is a valid subset.

All elements are prime

No element divides any other (since they are all > 1 and prime). The largest subset is any single element.

Single element

dp[0] = 1. The subset is just that element.

07

Complexity

Time
O(n²)
Space
O(n)
Double loop over the sorted array. Parent array and dp array each use O(n).