title: Partition Equal Subset Sum slug: partition-equal-subset-sum difficulty: medium leetcode_id: 416 leetcode_url: https://leetcode.com/problems/partition-equal-subset-sum/ categories: - arrays - dynamic-programming patterns: - dynamic-programming function_signature: "def can_partition(nums: list[int]) -> bool:" test_cases: visible: - input: { nums: [1, 5, 11, 5] } expected: true - input: { nums: [1, 2, 3, 5] } expected: false - input: { nums: [1, 2, 5] } expected: false hidden: - input: { nums: [1, 1] } expected: true - input: { nums: [2, 2, 1, 1] } expected: true - input: { nums: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99, 97] } expected: false description: | Given an integer array `nums`, return `true` *if you can partition the array into two subsets such that the sum of the elements in both subsets is equal* or `false` *otherwise*. constraints: | - `1 <= nums.length <= 200` - `1 <= nums[i] <= 100` examples: - input: "nums = [1,5,11,5]" output: "true" explanation: "The array can be partitioned as [1, 5, 5] and [11]." - input: "nums = [1,2,3,5]" output: "false" explanation: "The array cannot be partitioned into equal sum subsets." explanation: intuition: | At first glance, this looks like a partitioning problem where we need to divide an array into two groups. But here's the key insight: if we can partition the array into two subsets with **equal sums**, then each subset must sum to exactly **half of the total array sum**. Think of it like balancing a scale. If the total weight is 22, each side needs exactly 11 to balance. This transforms our problem: instead of finding two equal subsets, we just need to find **one subset that sums to `total_sum / 2`**. The remaining elements automatically form the other subset. This is a classic **0/1 Knapsack** problem in disguise. Imagine you have a knapsack with capacity `total_sum / 2`. Each number in the array is an item you can either include or exclude. Can you fill the knapsack exactly? Two immediate observations help us prune: - If the total sum is **odd**, it's impossible to split into two equal integer sums — return `false` immediately - If any single element exceeds `total_sum / 2`, it can't fit in either subset — return `false` approach: | We use **Dynamic Programming** with a boolean array to track achievable sums. **Step 1: Calculate the target** - Compute `total_sum` of all elements - If `total_sum` is odd, return `false` immediately (can't split evenly) - Set `target = total_sum // 2`   **Step 2: Initialise the DP array** - Create a boolean array `dp` of size `target + 1` - `dp[i]` represents: "Can we achieve sum `i` using some subset of numbers seen so far?" - Set `dp[0] = True` — we can always achieve sum 0 by selecting nothing   **Step 3: Process each number** - For each `num` in the array, iterate **backwards** from `target` down to `num` - For each sum `j`, if `dp[j - num]` is `True`, then `dp[j]` becomes `True` - We iterate backwards to avoid using the same number twice in one iteration   **Step 4: Return the result** - Return `dp[target]` — whether we can achieve exactly half the total sum   The backward iteration is crucial: if we went forward, adding a number could affect later calculations in the same pass, effectively "using" the number multiple times. common_pitfalls: - title: Forward Iteration Leads to Reusing Elements description: | A subtle but critical bug occurs if you iterate forward through sums instead of backward. Consider `nums = [1, 2]` with `target = 3`. If we process `1` going forward: - `dp[1] = True` (we can make 1) - Then checking `dp[2]`: `dp[2 - 1] = dp[1] = True`, so `dp[2] = True` But wait — we've effectively used `1` twice! The backward iteration prevents this by ensuring we only consider sums achievable *before* adding the current number. wrong_approach: "Forward iteration: for j in range(num, target + 1)" correct_approach: "Backward iteration: for j in range(target, num - 1, -1)" - title: Forgetting the Odd Sum Early Exit description: | If the total sum is odd (e.g., 11), there's no way to split it into two equal integer parts. Without this check, the algorithm would waste time computing a DP table for an impossible target. Always check `if total_sum % 2 != 0: return False` before proceeding. wrong_approach: "Skipping the odd check and computing DP anyway" correct_approach: "Return False immediately when total_sum is odd" - title: Not Handling Large Single Elements description: | If any element exceeds `target`, it cannot be part of either subset that sums to `target`. While the DP naturally handles this (we skip sums less than `num`), an explicit check can provide an early exit. For `nums = [1, 2, 100]` with `total_sum = 103`, this is odd so we'd exit anyway. But for `nums = [1, 100, 1]` with `total_sum = 102` and `target = 51`, the `100` makes partitioning impossible. key_takeaways: - "**Subset sum is 0/1 Knapsack**: Recognise that finding a subset with a target sum is equivalent to the classic knapsack problem" - "**Transform the problem**: Instead of finding two equal subsets, find one subset summing to half the total — much simpler" - "**Backward DP iteration**: When each element can only be used once, iterate backwards to prevent double-counting" - "**Early pruning matters**: Odd total sums are impossible; check this first for an O(1) exit in many cases" time_complexity: "O(n × target) where `n` is the array length and `target = sum(nums) / 2`. We process each number once and update up to `target` sums." space_complexity: "O(target). We use a 1D DP array of size `target + 1`. This can be up to O(n × max_element / 2) = O(n × 50) = O(n × 50) in the worst case given the constraints." solutions: - approach_name: 1D Dynamic Programming is_optimal: true code: | def can_partition(nums: list[int]) -> bool: total_sum = sum(nums) # Odd sum can't be split into two equal parts if total_sum % 2 != 0: return False target = total_sum // 2 # dp[i] = True if we can achieve sum i with some subset dp = [False] * (target + 1) dp[0] = True # Sum of 0 is always achievable (empty subset) for num in nums: # Iterate backwards to avoid using same num twice for j in range(target, num - 1, -1): # If we could make (j - num), we can now make j if dp[j - num]: dp[j] = True # Early exit if we've found the target if dp[target]: return True return dp[target] explanation: | **Time Complexity:** O(n × target) — For each of the n numbers, we potentially update target sums. **Space Complexity:** O(target) — Single array of size target + 1. This optimised 1D approach uses the insight that we only need the previous row's values, and by iterating backwards, we can update in place without a second array. - approach_name: 2D Dynamic Programming is_optimal: false code: | def can_partition(nums: list[int]) -> bool: total_sum = sum(nums) # Odd sum can't be split into two equal parts if total_sum % 2 != 0: return False target = total_sum // 2 n = len(nums) # dp[i][j] = True if first i elements can sum to j dp = [[False] * (target + 1) for _ in range(n + 1)] # Base case: sum 0 is achievable with any number of elements for i in range(n + 1): dp[i][0] = True for i in range(1, n + 1): num = nums[i - 1] for j in range(1, target + 1): # Don't include current number dp[i][j] = dp[i - 1][j] # Include current number if it fits if j >= num: dp[i][j] = dp[i][j] or dp[i - 1][j - num] return dp[n][target] explanation: | **Time Complexity:** O(n × target) — Same as 1D approach. **Space Complexity:** O(n × target) — Full 2D table. This classic 2D DP makes the state transitions clearer: for each element, we either include it (look at `dp[i-1][j-num]`) or exclude it (look at `dp[i-1][j]`). While easier to understand, it uses more memory than necessary. - approach_name: Recursive with Memoization is_optimal: false code: | def can_partition(nums: list[int]) -> bool: total_sum = sum(nums) if total_sum % 2 != 0: return False target = total_sum // 2 memo = {} def dp(index: int, remaining: int) -> bool: # Base cases if remaining == 0: return True if index >= len(nums) or remaining < 0: return False # Check memo if (index, remaining) in memo: return memo[(index, remaining)] # Try including or excluding current element result = (dp(index + 1, remaining - nums[index]) or dp(index + 1, remaining)) memo[(index, remaining)] = result return result return dp(0, target) explanation: | **Time Complexity:** O(n × target) — Each unique (index, remaining) state computed once. **Space Complexity:** O(n × target) for memoization + O(n) recursion stack. This top-down approach may be more intuitive for those who think recursively. At each index, we branch: either include the current number (subtract from remaining) or skip it. Memoization prevents recomputation of identical subproblems.