Partition Equal Subset Sum

Problem

Given a list of positive integers nums, return True if the array can be partitioned into two subsets such that the sums of the subsets are equal, and False otherwise.

  • 1 ≤ nums.length ≤ 200
  • 1 ≤ nums[i] ≤ 100

Example

Input: nums = [1, 5, 11, 5]
Output: True

The total sum is 22, which is even. The problem reduces to finding a subset that sums to 11. One valid subset is [11], and the other subset is [1, 5, 5], both summing to 11. The algorithm first checks if the total sum is even; if not, it returns False immediately. Then it uses recursion with memoization to explore whether a subset with sum equal to half the total exists. The recursion explores including or excluding each number, caching results to avoid redundant computations.

Approach

Straightforward Solution

A brute-force approach tries all subsets, which is exponential in time (O(2^n)) and infeasible for larger inputs.

Core Observation

The problem reduces to determining if there exists a subset of nums whose sum equals half of the total sum. This is because if such a subset exists, the rest of the elements form the other subset with the same sum.

Path to Optimal

Recognizing the problem as a subset sum problem, dynamic programming can be applied. The key insight is to use recursion with memoization to avoid recomputing states. The state is defined by the current index and the current sum accumulated. By caching results for each (index, current_sum) pair, the algorithm avoids exponential recomputation and achieves polynomial time complexity.

Optimal Approach

Use a top-down recursive DP with memoization. The recursion tries two choices at each index: include the current number in the subset or exclude it. If at any point the current sum equals the target (half the total sum), return True. If the current sum exceeds the target or the index goes beyond the array, return False. Memoize results to prune redundant paths. This approach guarantees correctness and efficiency.

Time

O(n * target)

The recursion explores states defined by index and current_sum. There are at most n indices and target sums, so the total states are O(n * target). Each state is computed once due to memoization.

Space

O(n * target)

The memo dictionary stores results for each (index, current_sum) state, requiring O(n * target) auxiliary space. The recursion stack depth is O(n), which is dominated by the memo size.

Pattern Spotlight

Dynamic Programming (Top-Down Memoized Subset Sum)

When searching for a subset with a target sum, model the problem as a state space of (index, current_sum) and use memoization to prune repeated computations, transforming exponential brute force into polynomial time.

Solution

1class Solution:
2 def canPartition(self, nums: list[int]) -> bool:
3 total_sum = sum(nums)
4 if total_sum % 2 != 0:
5 return False
6
7 target = total_sum // 2
8 memo = {}
9
10 def dp(i, current_sum):
11 if current_sum == target:
12 return True
13 if i >= len(nums) or current_sum > target:
14 return False
15 if (i, current_sum) in memo:
16 return memo[(i, current_sum)]
17
18 res = (dp(i + 1, current_sum + nums[i]) or
19 dp(i + 1, current_sum))
20
21 memo[(i, current_sum)] = res
22 return res
23
24 return dp(0, 0)

Step-by-Step Solution

1

Check Total Sum Parity and Initialize Memoization

3total_sum = sum(nums)
4if total_sum % 2 != 0:
5 return False
7target = total_sum // 2
8memo = {}

Objective

To quickly determine if partitioning is impossible due to odd total sum and prepare memoization storage for DP.

Key Insight

If the total sum is odd, it's impossible to split the array into two equal subsets, so the function returns False immediately. This early exit avoids unnecessary computation. Initializing a memo dictionary prepares for caching recursive results, which is essential for pruning redundant computations in the DP recursion.

Interview Quick-Check

Core Logic

Checking total sum parity is a constant-time filter that prevents futile recursion when partitioning is impossible.

Common Pitfalls & Bugs

Forgetting to check sum parity leads to wasted computation and incorrect results on odd sums.

State & Boundaries

Memoization dictionary keys are tuples of (index, current_sum), representing unique recursion states.

2

Recursively Explore Subsets with Memoized DP

10def dp(i, current_sum):
11 if current_sum == target:
12 return True
13 if i >= len(nums) or current_sum > target:
14 return False
15 if (i, current_sum) in memo:
16 return memo[(i, current_sum)]
18 res = (dp(i + 1, current_sum + nums[i]) or
19 dp(i + 1, current_sum))
21 memo[(i, current_sum)] = res
22 return res

Objective

To determine if a subset summing to target exists by recursively including or excluding each number, while caching results to avoid recomputation.

Key Insight

The recursion models the subset sum problem as a decision at each index: include the current number or skip it. The base cases handle success (current_sum equals target) and failure (index out of bounds or sum exceeded). Memoization stores results for each (index, current_sum) state, pruning repeated exploration of identical subproblems. This transforms an exponential search into a polynomial-time DP solution.

Interview Quick-Check

Core Logic

The recursion tries both including and excluding the current number, returning True if either leads to a valid subset sum.

State & Boundaries

Base cases ensure recursion stops when the target sum is reached or when the search space is exhausted.

Common Pitfalls & Bugs

Failing to memoize leads to exponential time due to repeated state exploration.

Complexity

Memoization reduces the time complexity from O(2^n) to O(n * target) by caching results.

3

Return Final Result from Recursive DP

24return dp(0, 0)

Objective

To initiate the recursive search from the first index with zero accumulated sum and return whether a valid partition exists.

Key Insight

Starting the recursion at index 0 and current_sum 0 explores all subsets systematically. The final return value reflects whether any subset sums to the target, which directly answers the partition problem. This call triggers the entire memoized DP process.

Interview Quick-Check

Core Logic

The initial call sets the recursion in motion, exploring all subset combinations from the start.

Line Analysis

This solution has 5 Critical lines interviewers watch for.

Line 11 Critical
if current_sum == target:

Return True if current_sum equals target.

Reaching the target sum means a valid subset has been found, so the recursion can terminate successfully.

Line 15 Critical
if (i, current_sum) in memo:

Recursively explore including or excluding the current number.

This line embodies the core decision: either add nums[i] to the current subset or skip it, exploring all subset combinations.

Line 4 Critical
if total_sum % 2 != 0:

Check if the total sum is odd.

If the total sum is odd, the array cannot be split into two equal subsets, so the function returns False immediately, avoiding unnecessary computation.

Line 12 Critical
return True

Return False if index is out of bounds or current_sum exceeds target.

These base cases prune invalid paths: exceeding the target or exhausting the array without success means no valid subset exists along this path.

Line 13 Critical
if i >= len(nums) or current_sum > target:

Check if the current state (index, current_sum) is memoized.

Memoization lookup prevents redundant computation by returning cached results for previously solved states.

Line 5 Core
return False

Return False when total sum is odd.

This early return prevents futile recursive exploration when partitioning is impossible due to sum parity.

Line 8 Core
memo = {}

Initialize a memoization dictionary to cache recursion results.

Memoization is essential to avoid recomputing the same (index, current_sum) states, transforming exponential recursion into polynomial time.

Line 14 Core
return False

Return the cached result if found in memo.

Returning cached results prunes the recursion tree, ensuring each state is computed once, which is critical for efficiency.

Line 16 Core
return memo[(i, current_sum)]

Store the result of the recursive exploration in memo.

Caching the result for the current state ensures future calls with the same parameters return instantly, preventing exponential blowup.

Line 19 Core
dp(i + 1, current_sum))

Recursive call to include the current number in the subset.

Including the current number advances the search towards the target sum by increasing current_sum.

Line 21 Core
memo[(i, current_sum)] = res

Recursive call to exclude the current number from the subset.

Excluding the current number explores alternative subsets that might reach the target without it.

Line 22 Core
return res

Memoize the combined result of including or excluding the current number.

Storing the OR of both recursive paths captures whether any valid subset exists from this state.

Line 3 Core
total_sum = sum(nums)

Calculate the total sum of all numbers in the input list.

The total sum determines if partitioning into two equal subsets is possible; an odd sum immediately disqualifies the input.

Line 7 Core
target = total_sum // 2

Calculate the target sum as half of the total sum.

The problem reduces to finding a subset that sums exactly to this target, enabling the partition.

Line 10 Core
def dp(i, current_sum):

Define the recursive function dp with parameters index and current_sum.

This function explores whether a subset starting at index can reach the target sum by accumulating current_sum, forming the core of the top-down DP approach.

Line 18 Core
res = (dp(i + 1, current_sum + nums[i]) or

Return the computed result for the current state.

This returns the answer up the recursion stack, enabling the final determination of partition feasibility.

Line 24 Core
return dp(0, 0)

Start the recursion from index 0 and current sum 0.

This initiates the DP exploration of all subsets, returning whether a valid partition exists.

Test Your Understanding

Why is it necessary to memoize the results of subproblems defined by (index, current_sum) in this recursive solution?

Because without memoization, the recursion would recompute the same states multiple times, leading to exponential time complexity. Memoization ensures each unique state is computed once, pruning redundant exploration and enabling polynomial time.

Related Problems

Dynamic Programming pattern

Don't just read it. Drill it.

Reconstruct Partition Equal Subset Sum from memory until it sticks. AlgoDrill blanks out key lines and makes you fill them back in, step by step.

Unlock the Partition Equal Subset Sum drill

or drill a free problem