416. Partition Equal Subset Sum
Problem Description
You are given an integer array nums
. Your task is to determine if you can split this array into two subsets where both subsets have equal sums.
For example, if you have the array [1, 5, 11, 5]
, you can partition it into [1, 5, 5]
and [11]
, where both subsets sum to 11. Therefore, the answer would be true
.
However, if you have the array [1, 2, 3, 5]
, the total sum is 11 (which is odd), so it's impossible to split it into two equal-sum subsets. The answer would be false
.
The problem asks you to return true
if such a partition is possible, and false
otherwise.
Key observations:
- If the total sum of the array is odd, it's impossible to partition it into two equal subsets
- If the total sum is even, the problem becomes: can we find a subset that sums to exactly half of the total sum?
- This is essentially a subset sum problem where the target is
sum(nums) / 2
Intuition
The key insight is recognizing that if we can partition an array into two subsets with equal sums, then each subset must have a sum equal to exactly half of the total array sum. This immediately tells us that if the total sum is odd, partitioning is impossible.
When the total sum is even, we need to find if there's a subset that sums to target = total_sum / 2
. If such a subset exists, the remaining elements will automatically form another subset with the same sum.
This transforms our problem into the classic "Subset Sum" problem: can we select some elements from the array that sum to a specific target?
To solve this, we can think about it step by step. For each element in the array, we have two choices: either include it in our subset or exclude it. This leads us to think about dynamic programming.
We can build up our solution by considering: "Can we achieve sum j
using the first i
elements?" For each element x
at position i
:
- If we don't include
x
, then achieving sumj
depends on whether we could achieve sumj
using the firsti-1
elements - If we include
x
, then achieving sumj
depends on whether we could achieve sumj-x
using the firsti-1
elements (we needj-x
from previous elements plusx
to makej
)
Starting from the base case where we can achieve sum 0 with 0 elements, we can build up a table that tells us whether each sum is achievable with each subset of elements, ultimately answering whether sum target
is achievable using all available elements.
Solution Approach
We implement the solution using dynamic programming with a 2D boolean array.
First, we calculate the total sum of nums
and check if it's even:
m, mod = divmod(sum(nums), 2)
if mod:
return False
If the sum is odd (mod != 0
), we return false
immediately. Otherwise, our target sum is m = sum(nums) // 2
.
Next, we create a 2D DP table f
where f[i][j]
represents whether we can achieve sum j
using the first i
numbers:
f = [[False] * (m + 1) for _ in range(n + 1)]
The table has dimensions (n+1) × (m+1)
to handle the base case and all possible sums from 0 to m
.
We initialize the base case:
f[0][0] = True
This means we can achieve sum 0 using 0 elements (by selecting nothing).
Now we fill the DP table iteratively. For each number x
at position i
(1-indexed) and each possible sum j
from 0 to m
:
for i, x in enumerate(nums, 1):
for j in range(m + 1):
f[i][j] = f[i - 1][j] or (j >= x and f[i - 1][j - x])
The state transition equation is:
f[i][j] = f[i - 1][j]
- we don't select the current numberx
, so we inherit the result from using the firsti-1
numbers- If
j >= x
, we can also try selectingx
:f[i][j] = f[i][j] or f[i - 1][j - x]
- we check if we could achieve sumj - x
with the firsti-1
numbers
The condition j >= x
ensures we only try to include x
when the target sum j
is at least as large as x
.
Finally, we return f[n][m]
, which tells us whether we can achieve sum m
using all n
numbers (though we don't have to use all of them - we just have them available for selection).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the example nums = [1, 5, 11, 5]
to illustrate the solution approach.
Step 1: Check if partition is possible
- Total sum = 1 + 5 + 11 + 5 = 22 (even)
- Target sum for each subset = 22 / 2 = 11
- Since the sum is even, we proceed with DP
Step 2: Initialize DP table
We create a table f[i][j]
where i
represents "using first i numbers" and j
represents "achieving sum j".
- Dimensions: 5 rows (0 to 4 numbers) × 12 columns (sums 0 to 11)
- Base case:
f[0][0] = True
(we can achieve sum 0 with 0 numbers)
Step 3: Fill the DP table iteratively
Processing nums[0] = 1
(i = 1):
- For each sum j from 0 to 11:
f[1][0] = f[0][0] = True
(don't take 1)f[1][1] = f[0][0] = True
(take 1: 0 + 1 = 1)f[1][2...11] = False
(can't achieve these sums with just 1)
Processing nums[1] = 5
(i = 2):
- Notable updates:
f[2][0] = True
(don't take anything)f[2][1] = True
(inherited from f[1][1])f[2][5] = f[1][0] = True
(take 5: 0 + 5 = 5)f[2][6] = f[1][1] = True
(take 5: 1 + 5 = 6)
Processing nums[2] = 11
(i = 3):
- Notable updates:
- Previous sums remain achievable
f[3][11] = f[2][0] = True
(take 11: 0 + 11 = 11) ✓
At this point, f[3][11] = True
, meaning we can achieve sum 11 using the first 3 numbers. We could stop here, but let's complete the table.
Processing nums[3] = 5
(i = 4):
f[4][11]
remainsTrue
(inherited from f[3][11])
Step 4: Return result
f[4][11] = True
, confirming we can partition the array into two equal-sum subsets.
Indeed, we can form subset [1, 5, 5]
with sum 11, leaving [11]
with sum 11.
Solution Implementation
1class Solution:
2 def canPartition(self, nums: List[int]) -> bool:
3 # Calculate total sum and check if it can be divided into two equal parts
4 total_sum = sum(nums)
5 target, remainder = divmod(total_sum, 2)
6
7 # If sum is odd, we cannot partition into two equal subsets
8 if remainder != 0:
9 return False
10
11 n = len(nums)
12
13 # DP table: dp[i][j] represents whether we can achieve sum j using first i numbers
14 # dp[i][j] = True if we can make sum j using nums[0...i-1]
15 dp = [[False] * (target + 1) for _ in range(n + 1)]
16
17 # Base case: we can always make sum 0 with empty subset
18 dp[0][0] = True
19
20 # Fill the DP table
21 for i in range(1, n + 1):
22 current_num = nums[i - 1] # Current number (adjust for 0-indexed nums array)
23
24 for j in range(target + 1):
25 # Two choices for each number:
26 # 1. Don't include current number: dp[i-1][j]
27 # 2. Include current number (if possible): dp[i-1][j-current_num]
28 dp[i][j] = dp[i - 1][j] # Don't include current number
29
30 if j >= current_num: # Can include current number
31 dp[i][j] = dp[i][j] or dp[i - 1][j - current_num]
32
33 # Return whether we can achieve the target sum using all n numbers
34 return dp[n][target]
35
1class Solution {
2 public boolean canPartition(int[] nums) {
3 // Calculate the total sum of all elements
4 int totalSum = 0;
5 for (int num : nums) {
6 totalSum += num;
7 }
8
9 // If total sum is odd, we cannot partition into two equal subsets
10 if (totalSum % 2 == 1) {
11 return false;
12 }
13
14 int n = nums.length;
15 int targetSum = totalSum / 2; // Each subset should sum to half of total
16
17 // dp[i][j] represents whether we can achieve sum j using first i elements
18 boolean[][] dp = new boolean[n + 1][targetSum + 1];
19
20 // Base case: we can always achieve sum 0 with 0 elements
21 dp[0][0] = true;
22
23 // Fill the DP table
24 for (int i = 1; i <= n; i++) {
25 int currentNum = nums[i - 1];
26
27 for (int j = 0; j <= targetSum; j++) {
28 // Option 1: Don't include current number
29 dp[i][j] = dp[i - 1][j];
30
31 // Option 2: Include current number (if it doesn't exceed target)
32 if (j >= currentNum) {
33 dp[i][j] = dp[i][j] || dp[i - 1][j - currentNum];
34 }
35 }
36 }
37
38 // Return whether we can achieve targetSum using all n elements
39 return dp[n][targetSum];
40 }
41}
42
1class Solution {
2public:
3 bool canPartition(vector<int>& nums) {
4 // Calculate the total sum of all elements
5 int totalSum = accumulate(nums.begin(), nums.end(), 0);
6
7 // If total sum is odd, we cannot partition into two equal subsets
8 if (totalSum % 2 == 1) {
9 return false;
10 }
11
12 int n = nums.size();
13 int targetSum = totalSum / 2; // Target sum for each subset
14
15 // dp[i][j] represents whether we can achieve sum j using first i elements
16 bool dp[n + 1][targetSum + 1];
17 memset(dp, false, sizeof(dp));
18
19 // Base case: we can always achieve sum 0 with 0 elements
20 dp[0][0] = true;
21
22 // Fill the DP table
23 for (int i = 1; i <= n; ++i) {
24 int currentNum = nums[i - 1];
25
26 for (int j = 0; j <= targetSum; ++j) {
27 // Two choices for each element:
28 // 1. Don't include current element: dp[i-1][j]
29 // 2. Include current element (if possible): dp[i-1][j-currentNum]
30 dp[i][j] = dp[i - 1][j] || (j >= currentNum && dp[i - 1][j - currentNum]);
31 }
32 }
33
34 // Return whether we can achieve targetSum using all n elements
35 return dp[n][targetSum];
36 }
37};
38
1/**
2 * Determines if the array can be partitioned into two subsets with equal sum
3 * @param nums - Array of positive integers
4 * @returns true if the array can be partitioned into two equal sum subsets, false otherwise
5 */
6function canPartition(nums: number[]): boolean {
7 // Calculate the total sum of all elements
8 const totalSum: number = nums.reduce((accumulator, current) => accumulator + current, 0);
9
10 // If the total sum is odd, we cannot partition into two equal subsets
11 if (totalSum % 2 === 1) {
12 return false;
13 }
14
15 // Get the array length
16 const arrayLength: number = nums.length;
17
18 // Calculate the target sum for each subset (half of total sum)
19 const targetSum: number = totalSum >> 1; // Using bit shift for division by 2
20
21 // Create a 2D DP table where dp[i][j] represents whether we can achieve sum j
22 // using the first i elements from the array
23 const dp: boolean[][] = Array.from(
24 { length: arrayLength + 1 },
25 () => Array(targetSum + 1).fill(false)
26 );
27
28 // Base case: we can always achieve sum 0 with 0 elements
29 dp[0][0] = true;
30
31 // Fill the DP table
32 for (let i = 1; i <= arrayLength; ++i) {
33 // Get the current element (adjusting for 0-based indexing)
34 const currentNum: number = nums[i - 1];
35
36 // Check all possible sums from 0 to targetSum
37 for (let j = 0; j <= targetSum; ++j) {
38 // We have two choices:
39 // 1. Don't include the current number: dp[i-1][j]
40 // 2. Include the current number (if possible): dp[i-1][j-currentNum]
41 dp[i][j] = dp[i - 1][j] || (j >= currentNum && dp[i - 1][j - currentNum]);
42 }
43 }
44
45 // Return whether we can achieve the target sum using all elements
46 return dp[arrayLength][targetSum];
47}
48
Time and Space Complexity
The time complexity is O(m × n)
, where m
is half of the total sum of the array and n
is the length of the array. The algorithm uses a 2D dynamic programming approach with a nested loop structure - the outer loop iterates through all n
elements in the array, and the inner loop iterates through all possible sums from 0
to m
. Each cell computation f[i][j] = f[i - 1][j] or (j >= x and f[i - 1][j - x])
takes O(1)
time, resulting in an overall time complexity of O(m × n)
.
The space complexity is O(m × n)
. The algorithm creates a 2D boolean array f
with dimensions (n + 1) × (m + 1)
to store the dynamic programming states. Each cell in this array represents whether it's possible to achieve a sum of j
using the first i
elements from the input array. Since we're storing all intermediate results in this 2D table, the space requirement is proportional to O((n + 1) × (m + 1))
, which simplifies to O(m × n)
.
Common Pitfalls
1. Space Complexity Optimization Overlooked
The current solution uses O(n × target) space with a 2D DP table. Since each row only depends on the previous row, we can optimize this to O(target) space using a 1D array.
Issue: Using unnecessary memory when dealing with large arrays or large sum values.
Solution: Use a 1D DP array and iterate backwards to avoid overwriting values we still need:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
target, remainder = divmod(total_sum, 2)
if remainder != 0:
return False
# Use 1D DP array instead of 2D
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
# Iterate backwards to avoid using updated values
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
return dp[target]
2. Integer Overflow in Other Languages
While Python handles large integers automatically, in languages like Java or C++, calculating sum(nums)
could cause integer overflow for large arrays with large values.
Issue: Getting incorrect results due to overflow when calculating the total sum.
Solution: Use appropriate data types (like long
in Java) or check for overflow conditions:
// Java example long totalSum = 0; for (int num : nums) { totalSum += num; }
3. Early Termination Opportunities Missed
The current solution always processes all elements even when we've already found that we can achieve the target sum.
Issue: Wasting computation time when the answer could be determined earlier.
Solution: Add early termination checks:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
target, remainder = divmod(total_sum, 2)
if remainder != 0:
return False
# Early termination: if any single element equals target
if target in nums:
return True
# Early termination: if largest element > target
if max(nums) > target:
return False
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
# Early termination: check if target is achieved
if dp[target]:
return True
return dp[target]
4. Not Handling Edge Cases
Failing to handle special cases like empty arrays or arrays with single elements.
Issue: Runtime errors or incorrect results for edge cases.
Solution: Add edge case checks at the beginning:
def canPartition(self, nums: List[int]) -> bool:
# Edge cases
if not nums or len(nums) < 2:
return False
total_sum = sum(nums)
target, remainder = divmod(total_sum, 2)
if remainder != 0:
return False
# Continue with DP solution...
5. Incorrect Index Mapping in 2D DP
When using 2D DP, confusion between 0-indexed arrays and 1-indexed DP table can lead to off-by-one errors.
Issue: Accessing nums[i]
instead of nums[i-1]
when i
represents the DP table row (1-indexed).
Solution: Be consistent with indexing and add clear comments:
# Correct indexing
for i in range(1, n + 1):
current_num = nums[i - 1] # i is 1-indexed for DP, nums is 0-indexed
for j in range(target + 1):
dp[i][j] = dp[i - 1][j]
if j >= current_num:
dp[i][j] = dp[i][j] or dp[i - 1][j - current_num]
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!