Skip to content
dsa450for five year olds

Backtracking · 8 of 19

Subset Sum Problem

Decide if you can pick some of the numbers so that they add up to exactly the target.

Simple way
time O(2^n)
room O(n)
Clever way
time O(n * target)
room O(target)

The problem, simply

You get a list of non-negative numbers and a target. Can you choose some of the numbers, each at most once, so that they add up to exactly the target? Answer true or false.

Choosing no numbers at all is allowed, and it adds up to 0.

A story to remember it

Picture it

tree
34xx4x9626959
arraynumbers
01233452

Each number is a fork in the road: take it or skip it. A branch with a number means take it, and x means skip it. The circles show how much is still needed. With 4 numbers there are 2^4 = 16 ways to go.

  • looking now

Examples

A small pick works

In
nums = [3, 34, 4, 12, 5, 2], target = 9
Out
true

4 + 5 = 9. So does 3 + 4 + 2.

Too big

In
nums = [3, 34, 4, 12, 5, 2], target = 30
Out
false

Without 34 the most you can make is 26, and 34 alone is already too big.

Target zero

In
nums = [7, 8], target = 0
Out
true

Pick nothing. The empty pick adds up to 0.

All even, odd target

In
nums = [2, 4, 6], target = 5
Out
false

Even numbers always add up to an even total.

Way 1: the simple way

For each number, branch twice: take it, or skip it. Carry how much is still needed.

  1. If nothing is still needed, answer true.
  2. If the numbers ran out, or the need went below 0, this branch fails.
  3. Otherwise try taking the number (need goes down by it), and if that fails, try skipping it.
java
class Solution {
    public boolean isSubsetSum(int[] nums, int target) {
        return pick(nums, 0, target);
    }

    private boolean pick(int[] nums, int i, int need) {
        if (need == 0) return true;                       // exact match
        if (i == nums.length || need < 0) return false;   // out of numbers, or too much
        return pick(nums, i + 1, need - nums[i])          // take nums[i]
            || pick(nums, i + 1, need);                   // skip nums[i]
    }
}

How long it takes: two branches per number, O(2^n). Extra room: the call stack is n deep, O(n).

The tree asks the same small question many times: "using numbers from i on, can I make need?" There are only n * (target + 1) different such questions. That is the hint for Way 2.

Way 2: the clever way

Build a yes or no table. can[i][s] answers: using only the first i numbers, can I make the sum s?

  1. Row 0 uses no numbers, so only s = 0 is yes.
  2. For row i with the number x:
    • Skip x: if can[i - 1][s] was yes, it is still yes.
    • Take x: if s >= x and can[i - 1][s - x] was yes, then s is yes too.
  3. The answer is can[n][target].

Fill the yes table one number at a time

01 / 06

grid
0123456789noneuse 3use 4use 5use 2Y
arraynumbers
01233452

Row "none" means we may use no numbers. The only sum we can make is 0, so only that box says Y.

target
9
  • looking now
  • seen
  • done
  • answer
  • path
  • just added
  • ignored
java
class Solution {
    public boolean isSubsetSum(int[] nums, int target) {
        int n = nums.length;
        boolean[][] can = new boolean[n + 1][target + 1];
        can[0][0] = true;                                  // no numbers make only 0
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            for (int s = 0; s <= target; s++) {
                can[i][s] = can[i - 1][s]                  // skip x
                    || (s >= x && can[i - 1][s - x]);      // take x
            }
        }
        return can[n][target];
    }

    // the same idea with one row, walking s from big to small
    public boolean isSubsetSumOneRow(int[] nums, int target) {
        boolean[] can = new boolean[target + 1];
        can[0] = true;
        for (int x : nums) {
            for (int s = target; s >= x; s--) {
                if (can[s - x]) can[s] = true;             // big to small: x is used once
            }
        }
        return can[target];
    }
}

How long it takes: one box for every number and every sum, O(n * target). Extra room: the full table is O(n * target), and the one row version needs only O(target).

This is fast when the target is small. If the target is a billion, the table is too big, and the problem is NP-complete in general.

Watch out for

  • Forgetting can[0][0] = true. Then nothing can ever become true.
  • Negative numbers. The table index s - x breaks, and the "need below 0" stop in Way 1 is wrong. Shift the sums by the total of the negatives, or ask if negatives are allowed.
  • In Way 1, checking need < 0 before need == 0 is fine, but checking i == nums.length before need == 0 misses a match made with the last number.

What interviewers ask next

  • Print one subset. Fill the 2D table, then walk back from can[n][target]: if the box above is yes, skip that number; otherwise take it and move left by its value.
  • List every subset. Use Way 1 with a list that you add to and remove from, and save a copy whenever the need hits 0.
  • Count the subsets. Store counts instead of yes or no: ways[s] += ways[s - x], walking s from big to small.
  • Split into two equal halves (Partition Equal Subset Sum). Run this with target = total / 2, after checking the total is even.
  • Make the difference of two groups as small as possible. Fill the table up to total / 2 and take the biggest reachable sum s. The answer is total - 2s.

Remember this

Grown-up words: the take or skip tree is backtracking over subsets, the repeated small questions are overlapping subproblems, the yes table is a 2D dynamic programming table, and the one row trick is the 0/1 knapsack space saving.