Medium Problemssubsetcounting

All distinct subset (or subsequence) sums

Given a set of n non-negative integers, find every distinct total that can be formed by adding up some subset of its elements, including the empty subset's total of zero.

Do this lesson first: coin change

Example input

nums = [2, 3, 5]

Expected output

0, 2, 3, 5, 7, 8, 10

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.

Step not started

Press start. The animation stops at every cell YOUR recurrence must fill.

Row 0 is the only base row: with no numbers allowed the only subset is the empty one, so dp[0][0] = 1 and every other cell of row 0 is 0. Column 0 computes, and is 1 all the way down, since leaving everything out always makes 0; that is the empty subset's total the statement asks you to include. Fill row by row. Both cells a cell reads are in the row above, and the put-it-in read has to come from the row above rather than the current one, or a number can be used twice.

Rows ↓
Cols →
0
1
2
3
4
5
6
7
8
9
10
none
1
0
0
0
0
0
0
0
0
0
0
2
1
0
1
0
0
0
0
0
0
0
0
3
1
0
1
1
0
1
0
0
0
0
0
5
1
0
1
1
0
1
0
1
1
0
1

Rows ↓row i: may use the first i numbers

Cols →total s: can some subset make s?

all-distinct-subset-sums.ts
  1. 1function allSubsetSums(nums) {
  2. 2 const n = nums.length;
  3. 3 const total = nums.reduce((a, b) => a + b, 0);
  4. 4 const dp = Array.from({ length: n + 1 }, () => new Array(total + 1).fill(0));
  5. 5 dp[0][0] = 1;
  6. 6 for (let i = 1; i <= n; i++) {
  7. 7 const a = nums[i - 1];
  8. 8 for (let s = 0; s <= total; s++) {
  9. 9 dp[i][s] = dp[i - 1][s];
  10. 10 if (a <= s) dp[i][s] = Math.max(dp[i][s], dp[i - 1][s - a]);
  11. 11 }
  12. 12 }
  13. 13 const sums = [];
  14. 14 for (let s = 0; s <= total; s++) if (dp[n][s] === 1) sums.push(s);
  15. 15 return sums;
  16. 16}
Base caseComputedBeing readAnswer

The code, the trap, the variations

all-distinct-subset-sums.ts
  1. 1function allSubsetSums(nums) {
  2. 2 const n = nums.length;
  3. 3 const total = nums.reduce((a, b) => a + b, 0);
  4. 4 const dp = Array.from({ length: n + 1 }, () => new Array(total + 1).fill(0));
  5. 5 dp[0][0] = 1;
  6. 6 for (let i = 1; i <= n; i++) {
  7. 7 const a = nums[i - 1];
  8. 8 for (let s = 0; s <= total; s++) {
  9. 9 dp[i][s] = dp[i - 1][s];
  10. 10 if (a <= s) dp[i][s] = Math.max(dp[i][s], dp[i - 1][s - a]);
  11. 11 }
  12. 12 }
  13. 13 const sums = [];
  14. 14 for (let s = 0; s <= total; s++) if (dp[n][s] === 1) sums.push(s);
  15. 15 return sums;
  16. 16}

Where people go wrong

Letting a number be used twice. Read the put-it-in cell from the current row, dp[i][s - a_i], instead of the row above, and a number can join a subset that already holds it. On [2, 3, 5] the bottom row then claims 4 (2 + 2), 6 (3 + 3) and 9 (2 + 2 + 5) as well, ten totals where the answer has seven: every total from 0 to 10 except 1. The output is still a sorted list of plausible-looking totals, so nothing looks broken; it is the right answer to a different problem, the one where each number may be reused. In the single-row version the same bug is sweeping s upwards.

  • Count how many subsets make each total instead of whether any does.

    Replace the or with a plus and keep the table, the base row and the order. dp[0][0] = 1 now counts the empty subset, and on [2, 3, 5] the bottom row reads 1, 0, 1, 1, 0, 2, 0, 1, 1, 0, 1: the 2 in column 5 is the duplicate this page's yes or no quietly merges. The row adds up to 2^n, here 8, one for every subset.

  • Allow zeros among the numbers, as the statement's non-negative integers do.

    A 0 changes nothing. Its put-it-in cell is the cell straight above, the same one its leave-it-out cell reads, so its row is a copy of the row above, and the distinct totals are exactly those of the other numbers. The usual code handles that without a special case; it is only worth dropping zeros first to save the copied rows.

  • Allow negative numbers.

    The totals now run from the sum of the negatives up to the sum of the positives, so shift every column by the size of the negative sum and the table is the same skip-or-take table over a wider range. The put-it-in cell for a negative a_i sits to the RIGHT of the cell rather than the left, which does not matter while it is read from the row above, and matters a great deal in the one-row version, where the sweep must then run left to right for that number.