Skip to content

416. Partition Equal Subset Sum (Medium)

Problem

Given a non-empty array nums of positive integers, determine if the array can be partitioned into two subsets such that the sum of elements in both subsets is equal.

Example

  • nums = [1, 5, 11, 5]true ([1, 5, 5] and [11])
  • nums = [1, 2, 3, 5]false

LeetCode 416 · Link · Medium

Try it yourself

idle

Starter code: this editor begins with intentional TODOs. Fill the function, run the embedded tests, then compare your solution with the worked approaches below.

Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 1: Brute force, enumerate subsets

Try every subset; test whether it sums to total / 2.

def can_partition(nums):
total = sum(nums) # L1: O(n)
if total % 2: # L2: O(1)
return False
target = total // 2 # L3: O(1)
def f(i, cur):
if cur == target: # L4: O(1)
return True
if i == len(nums) or cur > target: # L5: O(1)
return False
return f(i + 1, cur + nums[i]) or f(i + 1, cur) # L6: two recursive calls
return f(0, 0)

Where the time goes, line by line

Variables: n = len(nums), S = sum(nums) (we target S/2).

LinePer-call costTimes executedContribution
L1 (sum)O(n)O(n)1O(n)O(n)
L2, L3 (parity check)O(1)O(1)1O(1)O(1)
L4, L5 (base cases)O(1)O(1)per callO(2n)O(2ⁿ)
L6 (two recursive calls)O(1)O(1)up to 2ⁿO(2n)O(2ⁿ) ← dominates

Each item is either included or not, producing a full binary recursion tree of depth n. No memoization means identical subproblems are recomputed.

Complexity

  • Time: O(2n)O(2ⁿ), driven by L6 (the binary branching at every element).
  • Space: O(n)O(n) for the recursion stack.

Approach 2: Top-down memoized

Cache by (i, cur).

from functools import lru_cache
def can_partition(nums):
total = sum(nums) # L1: O(n)
if total % 2: # L2: O(1)
return False
target = total // 2 # L3: O(1)
@lru_cache(maxsize=None)
def f(i, cur):
if cur == target: # L4: O(1)
return True
if i == len(nums) or cur > target: # L5: O(1)
return False
return f(i + 1, cur + nums[i]) or f(i + 1, cur) # L6: cached calls
return f(0, 0)

Where the time goes, line by line

Variables: n = len(nums), S = sum(nums) (we target S/2).

LinePer-call costTimes executedContribution
L1 (sum)O(n)O(n)1O(n)O(n)
L2, L3 (parity check)O(1)O(1)1O(1)O(1)
L4, L5 (base cases)O(1)O(1)per unique (i, cur)O(ntarget)O(n · target)
L6 (recursive calls)O(1)O(1) amortizedn · target unique statesO(ntarget)O(n · target) ← dominates

With memoization, each (i, cur) pair is computed at most once. There are n indices and at most target+1 possible cur values, giving n · (target+1) unique states.

Complexity

  • Time: O(ntarget)O(n · target), driven by L6 (unique state count bounds total work).
  • Space: O(ntarget)O(n · target) for the memo table plus O(n)O(n) for the call stack.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Approach 3: Bottom-up 1-D DP (canonical, optimal space)

dp[s] = True iff some subset of processed items sums to s. Process items one at a time; iterate s from high to low to avoid reusing an item (0/1 knapsack rule).

def can_partition(nums):
total = sum(nums) # L1: O(n)
if total % 2: # L2: O(1)
return False
target = total // 2 # L3: O(1)
dp = [False] * (target + 1) # L4: O(target)
dp[0] = True # L5: O(1)
for x in nums: # L6: outer loop, n iterations
for s in range(target, x - 1, -1): # L7: inner loop, up to target iterations
dp[s] = dp[s] or dp[s - x] # L8: O(1) per cell
return dp[target] # L9: O(1)

Where the time goes, line by line

Variables: n = len(nums), S = sum(nums) (we target S/2).

LinePer-call costTimes executedContribution
L1 (sum)O(n)O(n)1O(n)O(n)
L4 (allocate dp)O(target)O(target)1O(target)O(target)
L6 (outer loop)O(1)O(1)nO(n)O(n)
L7, L8 (inner loop + update)O(1)O(1)n × targetO(ntarget)O(n · target) ← dominates
L9 (return)O(1)O(1)1O(1)O(1)

The double loop visits each of the n items against each of the target possible sums. The high-to-low sweep on L7 is not extra work; it is the same O(target)O(target) range traversed in reverse.

Complexity

  • Time: O(ntarget)O(n · target), driven by L7/L8 (the double loop).
  • Space: O(target)O(target) for the dp array.

Why iterate from high to low

In 0/1 knapsack, iterating s low-to-high would let an item be included more than once in the same outer iteration. Going high-to-low uses only values that haven’t yet been updated this round, preserving the 0/1 semantics.

Try this approach:

idle
Click Run Python to execute. First run downloads Python (~10 MB, cached after that).

Summary

ApproachTimeSpace
Naive recursionO(2n)O(2ⁿ)O(n)O(n)
Top-down memoO(ntarget)O(n · target)O(ntarget)O(n · target)
Bottom-up 1-D DPO(ntarget)O(n · target)O(target)O(target)

Template for every 0/1 knapsack problem, Target Sum (494), Last Stone Weight II (1049), etc.

Test cases

# Quick smoke tests, paste into a REPL or save as test_416.py and run.
# Uses the canonical implementation (Approach 3: bottom-up 1-D DP).
def can_partition(nums):
total = sum(nums)
if total % 2:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for x in nums:
for s in range(target, x - 1, -1):
dp[s] = dp[s] or dp[s - x]
return dp[target]
def _run_tests():
assert can_partition([1, 5, 11, 5]) == True # LeetCode example 1
assert can_partition([1, 2, 3, 5]) == False # LeetCode example 2
assert can_partition([1]) == False # single element, odd total
assert can_partition([2, 2]) == True # single element each side
assert can_partition([1, 2, 5]) == False # total=8, target=4, no subset
assert can_partition([3, 3, 3, 4, 5]) == True # target=9, subset [3,3,3]
print("all tests pass")
if __name__ == "__main__":
_run_tests()
  • Arrays, DP array indexed by subset sum
  • Knapsack Patterns, the choose or skip structure behind capacity, target, and subset states.
  • Dynamic Programming, the state and transition model for reusing answers to overlapping subproblems.