Subset Sum Problem
Given a set of n non-negative integers and a target value, decide whether some subset of the set adds up to exactly that target.
Do this lesson first: coin changeExample input
set = [3, 5, 7, 8], target = 6
Expected output
false (dp[4][6] = 0)
Break it down
Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.
Fill the table
The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.
Press start. The animation stops at every cell YOUR recurrence must fill.
Row 0 is the only base row. With no numbers allowed, the empty subset is the only subset, so dp[0][0] = 1 and the rest of row 0 is 0. Column 0 is not a base: it computes, and comes out 1 all the way down, because leaving every number out still makes 0. Fill row by row from the top. Both cells a cell reads sit in the row above, so they are final before anything reads them. It has to be the row ABOVE for the put-it-in case, not the current row: the current row's cell at s - a_i may already have used a_i, and reading it would let one number appear twice.
Cols →
Rows ↓row i: may use the first i numbers
Cols →target s: can some subset make s?
- 1
function subsetSum(nums, target) { - 2
const n = nums.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(target + 1).fill(0)); - 4
dp[0][0] = 1; - 5
for (let i = 1; i <= n; i++) { - 6
const a = nums[i - 1]; - 7
for (let s = 0; s <= target; s++) { - 8
dp[i][s] = dp[i - 1][s]; - 9
if (a <= s) dp[i][s] = Math.max(dp[i][s], dp[i - 1][s - a]); - 10
} - 11
} - 12
return dp[n][target] === 1; - 13
}
The code, the trap, the variations
- 1
function subsetSum(nums, target) { - 2
const n = nums.length; - 3
const dp = Array.from({ length: n + 1 }, () => new Array(target + 1).fill(0)); - 4
dp[0][0] = 1; - 5
for (let i = 1; i <= n; i++) { - 6
const a = nums[i - 1]; - 7
for (let s = 0; s <= target; s++) { - 8
dp[i][s] = dp[i - 1][s]; - 9
if (a <= s) dp[i][s] = Math.max(dp[i][s], dp[i - 1][s - a]); - 10
} - 11
} - 12
return dp[n][target] === 1; - 13
}
Where people go wrong
Letting a number be used twice. The put-it-in case must read dp[i - 1][s - a_i], from the row above. Read dp[i][s - a_i] from the current row instead and the 3 can join a subset that already holds the 3: on this set that answers yes at target 6, from 3 + 3, and yes at target 9, from 3 + 3 + 3, where the true answer at both is no. That one changed row index is the entire difference between this problem and its use-any-number-again version, the shape coin-change-count-ways uses, so the bug produces a correct program for the wrong problem and nothing crashes. In the single-row version the same bug is sweeping s from left to right.
Count the subsets that add to the target instead of deciding whether one exists.
Replace the or with a plus and keep everything else: the same two cells, the same row-above rule, the same fill order. dp[0][0] = 1 now means one subset, the empty one, and the answer is a count rather than a flag.
Allow each number to be used any number of times.
The put-it-in case reads the current row, dp[i][s - a_i], because a_i is still available after it has been used once. That is the shape of coin-change-count-ways and unbounded-knapsack. The table's shape and the fill order do not change, only which row the second cell sits in.
Ask whether the numbers split into two groups with equal totals.
That is partition-problem. If the total is odd the answer is no without a table; otherwise it is this exact table with the target set to half the total, because one group adding to half leaves the rest adding to the other half.