Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Partition Equal Subset Sum


Partition Equal Subset Sum is the interview's favourite 0/1 knapsack: each item is taken at most once, and a budget — here, a target sum — limits what fits. The whole family (Target Sum, Last Stone Weight II, the classic value knapsack) uses the same table.

It is also the problem where the space-saving step is a trap: one loop direction is right and the other silently solves a different problem.

0/1 knapsack, capacity 40000001515151501515203501515203501234nonew1 v15w3 v20w4 v30Each cell skips the item (copy the cell above) or takes it (its value plus the cell w to the left).
Partition Equal Subset Sum and Target Sum are this table with the value column renamed.

The problem

Given a list of positive integers, return True if it can be split into two groups with equal sums, and False otherwise. Every number goes into exactly one group.

  • [2, 3, 7, 8, 10] → True. Total 30; 7 + 8 = 15 and 2 + 3 + 10 = 15.
  • [1, 2, 5] → False. Total 8, so each half would need 4, and no subset makes 4.

Constraints: 1 ≤ n ≤ 200, 1 ≤ each number ≤ 100.

Clarifying questions

  • Must both groups be non-empty? With positive numbers and equal sums they always are.
  • Can numbers repeat? Yes, but each position is used once.
  • Zero or negative numbers? Not here; negatives would break the "sum up to a target" table.

The reframe

If the total is S, each group must sum to S / 2. So an odd total is False at once. Otherwise the question becomes: is there a subset that sums to exactly S / 2? The other group is simply everything else.

That is 0/1 knapsack with value equal to weight, asking only whether the capacity can be filled exactly.

Approach 1: the simple way — take or skip each number

Python
def part_rec(nums: list[int]) -> bool:    """can(i, remaining): can nums[i:] make exactly `remaining`?"""    total = sum(nums)    if total % 2:        return False    def can(i: int, remaining: int) -> bool:        if remaining == 0:            return True        if i == len(nums) or remaining < 0:            return False        return can(i + 1, remaining - nums[i]) or can(i + 1, remaining)  # take or skip    return can(0, total // 2)

Why it is too slow. It explores up to 2ⁿ take-or-skip choices. When no split exists it must try them all: for [4, 2, 2, …] with 20 numbers it makes 1,384,495 calls, and with 24 numbers 21,769,503. At n = 200 it is hopeless. But the pair (i, remaining) takes at most n × (S / 2 + 1) values — 200 × 10,001 at the limits — so the calls repeat.

The key insight

Only two facts about the past matter: how many numbers you have decided, and what sum they make. Which exact numbers made that sum is irrelevant. So:

Text
dp[i][s] = True if some subset of the first i numbers sums to exactly sdp[i][s] = dp[i - 1][s]                       (skip number i)        or dp[i - 1][s - nums[i - 1]]         (take it, if it fits)dp[i][0] = True        — the empty subset makes 0

The i − 1 in the "take" branch is what makes it 0/1: after taking number i, you look at a row that no longer offers it. The combine step is or, because the question is "is it possible".

Approach 2: memoisation

Python
from functools import cachedef part_memo(nums: list[int]) -> bool:    total = sum(nums)    if total % 2:        return False    @cache    def can(i: int, remaining: int) -> bool:        if remaining == 0:            return True        if i == len(nums) or remaining < 0:            return False        return can(i + 1, remaining - nums[i]) or can(i + 1, remaining)    return can(0, total // 2)

At most n × (S / 2 + 1) states, O(1) each: O(n × S) time and space. The or stops early once a subset is found.

Approach 3: the full table

Python
def part_table(nums: list[int]) -> bool:    total = sum(nums)    if total % 2:        return False    target, n = total // 2, len(nums)    dp = [[False] * (target + 1) for _ in range(n + 1)]   # dp[i][s]    for i in range(n + 1):        dp[i][0] = True                                    # empty subset makes 0    for i in range(1, n + 1):        x = nums[i - 1]        for s in range(1, target + 1):            dp[i][s] = dp[i - 1][s] or (x <= s and dp[i - 1][s - x])    return dp[n][target]

O(n × target) time and space: at the limits the total is at most 200 × 100 = 20,000, so the table has 200 × 10,000 = 2 million cells.

Approach 4: one row, backwards

Row i reads only row i − 1. Keep one row, reach[s], and update it for each number — but loop s from high to low.

Python
def can_partition(nums: list[int]) -> bool:    """0/1 knapsack in one row: is there a subset summing to half the total?"""    total = sum(nums)    if total % 2:        return False    target = total // 2    reach = [True] + [False] * target        # reach[s]: some subset so far makes s    for x in nums:        for s in range(target, x - 1, -1):   # backwards: each x used at most once            if reach[s - x]:                reach[s] = True        if reach[target]:            return True                      # stop as soon as half is reachable    return reach[target]

Why backwards? When reach[s] reads reach[s − x], the smaller index must still hold the value from before x was considered. Going down, s − x has not been touched in this pass yet. Going up, it may already be True because of x, and then x is counted twice.

Dry run on [2, 3, 7, 8, 10], target 15 — the reachable sums after each number:

numbernew sums this passall reachable sums
220, 2
33, 50, 2, 3, 5
77, 9, 10, 120, 2, 3, 5, 7, 9, 10, 12
88, 11, 13, 150, 2, 3, 5, 7, 8, 9, 10, 11, 12, 13, 15

15 becomes reachable when 8 is added to 7, so the function returns True without looking at 10. Complexity: O(n × target) time, O(target) space.

Four failures, in the order they occurState and base cases• The state misses a needed dimension• A base case set at the wrong index• The answer read from the wrong cellOrder of the loops• A cell read before it is computed• One rolled row iterated the wrong way• 0/1 knapsack needs capacity descending
Rolled into one row, 0/1 knapsack must count capacity downwards or an item gets used twice.

The forwards bug, on two numbers. Take [1, 5]: total 6, target 3, and the true answer is False. Loop forwards for x = 1: reach[1] becomes True from reach[0], then reach[2] from the new reach[1], then reach[3] from the new reach[2]. The single 1 has been used three times, and the function returns True. Forwards is the right loop for unbounded knapsack (Coin Change), where reuse is the point — so the direction alone decides which problem you solved.

A compact alternative in Python: keep the reachable sums as bits of one integer, reach |= reach << x for each number, and test bit target. Same O(n × target) bound, but the inner loop runs inside the integer shift, so it is much faster in practice.

Edge cases

  • Odd total — False before any table is built.
  • One number — its total is odd or, if even, half of it cannot be made from nothing else: False.
  • A number bigger than half the total — it can never be in the half-sum subset; the loop range range(target, x - 1, -1) is empty for it, so it is skipped naturally.
  • Large values — the cost depends on the size of the target, not on n alone. This is called pseudo-polynomial: with values up to 10⁹, the table is impossible even for five numbers.

Follow-ups

  • Classic knapsack with values — best[c] = max(best[c], best[c − w] + v), same backwards loop. With weights [1, 3, 4], values [15, 20, 30] and capacity 4, the answer is 35 (the first two items), as in the diagram's table.
  • Signs + and − to reach a target (Target Sum) — if P is the sum of the + numbers, P − (total − P) = target, so P = (total + target) / 2. Count the subsets with that sum: the same table with + instead of or, and dp[0] = 1.
  • Split to make the two sums as close as possible (Last Stone Weight II) — build reach up to half the total, then take the largest reachable s; the answer is total − 2s.

Check your understanding

0 of 2 answered

1.With nums = [1, 5], a forwards one-row loop returns True. Why?

2.What makes this solution "pseudo-polynomial"?