|
38 | 38 |
|
39 | 39 | meaning "reachable before (skip x)" or "reachable by taking x (from state s−x)". Base: `dp[0] = true` (empty subset sums to 0). Answer: `dp[target]`. |
40 | 40 |
|
41 | | -**Why descending?** Exactly the 0/1 argument: reading `dp[s - x]` in descending order guarantees it reflects *previous* items only, so each number is used at most once. (Try ascending and `nums = [1, 1]`, target 1 → you'd get `true` — the bug of double-using.) |
42 | | - |
43 | | -## Approach 1 — Brute force |
44 | | - |
45 | | -Enumerate all $2^n$ subsets, check sums. $2^{200}$ — the largest number most people will ever see in an interview setting. DP is the *only* reasonable answer. |
46 | | - |
47 | | -## Approach 2 — Top-down memoized DFS (the repo's first version) |
48 | | - |
49 | | -```kotlin |
50 | | -/** |
51 | | - * @param nums the array of positive integers |
52 | | - * @return true iff nums can be split into two subsets of equal sum |
53 | | - */ |
54 | | -fun canPartition(nums: IntArray): Boolean { |
55 | | - val sum = nums.sum() |
56 | | - if (sum % 2 != 0) return false |
57 | | - val target = sum / 2 |
58 | | - |
59 | | - // memo[i][s] = -1 unknown, 0 false, 1 true |
60 | | - val memo = Array(nums.size) { IntArray(target + 1) { -1 } } |
61 | | - |
62 | | - /** |
63 | | - * @param i the current item index |
64 | | - * @param currentSum the running sum of the chosen subset |
65 | | - * @return true iff a subset of nums[i..] can reach `target` from currentSum |
66 | | - */ |
67 | | - fun dfs(i: Int, currentSum: Int): Boolean = when { |
68 | | - currentSum == target -> true |
69 | | - i == nums.size -> false |
70 | | - memo[i][currentSum] != -1 -> memo[i][currentSum] == 1 |
71 | | - else -> ( |
72 | | - dfs(i + 1, currentSum + nums[i]) || // take nums[i] |
73 | | - dfs(i + 1, currentSum) // skip nums[i] |
74 | | - ).also { memo[i][currentSum] = if (it) 1 else 0 } |
75 | | - } |
76 | | - return dfs(0, 0) |
77 | | -} |
78 | | -``` |
79 | | - |
80 | | -## Approach 3 — Bottom-up boolean knapsack (optimal) |
81 | | - |
82 | | -```kotlin |
83 | | -/** |
84 | | - * @param nums the array of positive integers |
85 | | - * @return true iff nums can be split into two subsets of equal sum |
86 | | - */ |
87 | | -fun canPartitionBottomUp(nums: IntArray): Boolean { |
88 | | - val sum = nums.sum() |
89 | | - if (sum % 2 != 0) return false |
90 | | - |
91 | | - val target = sum / 2 |
92 | | - val dp = BooleanArray(target + 1).apply { this[0] = true } // empty subset sums to 0 |
93 | | - |
94 | | - for (num in nums) { |
95 | | - for (s in target downTo num) { // DESCENDING: 0/1 usage of each number |
96 | | - dp[s] = dp[s] || dp[s - num] |
97 | | - } |
98 | | - } |
99 | | - return dp[target] |
100 | | -} |
101 | | -``` |
102 | | - |
103 | | -```java |
104 | | -public class PartitionEqualSubsetSum { |
105 | | - /** |
106 | | - * @param nums the array of positive integers |
107 | | - * @return true iff nums can be split into two subsets of equal sum |
108 | | - */ |
109 | | - public boolean canPartition(int[] nums) { |
110 | | - int sum = 0; |
111 | | - for (int x : nums) sum += x; |
112 | | - if (sum % 2 != 0) return false; |
113 | | - |
114 | | - int target = sum / 2; |
115 | | - boolean[] dp = new boolean[target + 1]; |
116 | | - dp[0] = true; // empty subset sums to 0 |
117 | | - |
118 | | - for (int num : nums) { |
119 | | - for (int s = target; s >= num; s--) { // descending: 0/1 semantics |
120 | | - dp[s] = dp[s] || dp[s - num]; |
121 | | - } |
122 | | - } |
123 | | - return dp[target]; |
124 | | - } |
125 | | -} |
126 | | -``` |
127 | | - |
128 | | -```cpp |
129 | | -#include <vector> |
130 | | - |
131 | | -class PartitionEqualSubsetSum { |
132 | | -public: |
133 | | - /** |
134 | | - * @param nums the array of positive integers |
135 | | - * @return true iff nums can be split into two subsets of equal sum |
136 | | - */ |
137 | | - bool canPartition(const std::vector<int>& nums) { |
138 | | - int sum = 0; |
139 | | - for (int x : nums) sum += x; |
140 | | - if (sum % 2 != 0) return false; |
141 | | - |
142 | | - int target = sum / 2; |
143 | | - std::vector<bool> dp(target + 1, false); |
144 | | - dp[0] = true; |
145 | | - |
146 | | - for (int num : nums) { |
147 | | - for (int s = target; s >= num; s--) { // descending: 0/1 semantics |
148 | | - dp[s] = dp[s] || dp[s - num]; |
149 | | - } |
150 | | - } |
151 | | - return dp[target]; |
152 | | - } |
153 | | -}; |
154 | | -``` |
155 | | - |
156 | | -```python |
157 | | -def can_partition(nums: list[int]) -> bool: |
158 | | - """ |
159 | | - @param nums: the array of positive integers |
160 | | - @return: True iff nums can be split into two subsets of equal sum |
161 | | - """ |
162 | | - total = sum(nums) |
163 | | - if total % 2 != 0: |
164 | | - return False |
165 | | - |
166 | | - target = total // 2 |
167 | | - dp = [False] * (target + 1) |
168 | | - dp[0] = True # empty subset sums to 0 |
169 | | - |
170 | | - for num in nums: |
171 | | - for s in range(target, num - 1, -1): # descending: 0/1 semantics |
172 | | - dp[s] = dp[s] or dp[s - num] |
173 | | - return dp[target] |
174 | | -``` |
175 | | - |
176 | | -```rust |
177 | | -impl Solution { |
178 | | - /// @param nums the array of positive integers |
179 | | - /// @return true iff nums can be split into two subsets of equal sum |
180 | | - pub fn can_partition(nums: Vec<i32>) -> bool { |
181 | | - let sum: i32 = nums.iter().sum(); |
182 | | - if sum % 2 != 0 { |
183 | | - return false; |
184 | | - } |
185 | | - let target = (sum / 2) as usize; |
186 | | - let mut dp = vec![false; target + 1]; |
187 | | - dp[0] = true; // empty subset sums to 0 |
188 | | - |
189 | | - for num in nums { |
190 | | - let mut s = target; |
191 | | - while s >= num as usize { // descending: 0/1 semantics |
192 | | - dp[s] = dp[s] || dp[s - num as usize]; |
193 | | - s -= 1; |
194 | | - } |
195 | | - } |
196 | | - dp[target] |
197 | | - } |
198 | | -} |
199 | | -``` |
200 | | - |
201 | | -## Dry run |
202 | | - |
203 | | -**Input:** `nums = [1, 5, 11, 5]`. `sum = 22`, `target = 11`. |
204 | | - |
205 | | -``` |
206 | | -dp (booleans over sums 0..11): |
207 | | -initial: T F F F F F F F F F F F |
208 | | -after 1: T T F F F F F F F F F F (1 reachable) |
209 | | -after 5: T T F F F F T T F F F F (5 and 6 reachable) |
210 | | -after 11: T T F F F F T T F F F T (11 reachable -> dp[11]=true already!) |
211 | | -after 5 (2nd): T T F F F T T T T T T T (5,6,7,8,10,11 reachable) |
212 | | -Answer: dp[11] = true ✓ (subset {11} or {1,5,5}) |
213 | | -``` |
214 | | - |
215 | | -Trace the interesting update (second `5`, `s = 11`): |
216 | | - |
217 | | -``` |
218 | | -s=11: dp[11] = dp[11] || dp[6] = true || true -> stays true (already reachable via {11}) |
219 | | -s=10: dp[10] = dp[10] || dp[5] = false || true -> true ({5,5}) |
220 | | -``` |
221 | | - |
222 | | -**Why descending matters (the bug to dodge):** with `nums = [1, 1]` and `target = 1`, an ascending loop would do: `s=1: dp[1] = dp[1] || dp[0] = true` — fine so far; but for `target = 2` (nums=[1,1], sum=2, target=1 — no, target is 1)... take `nums=[2,2]`, target=2: descending: s=2: dp[2]=dp[0]=true (one 2). ✓. Ascending: s=2: dp[2] = dp[0] = true — but then... still only one pass through nums, so it's fine for one item per outer iteration. The real contamination: `nums=[2]`, target=4? Not applicable (sum 2). The classic failure: `nums=[1,1]`, sum=2, target=1: both loops give true correctly. Better example: `nums = [2, 4]`, target=3: nothing reaches 3 → false either way. Hmm — the descending requirement is really about *within one outer iteration*: `nums=[2, 2, 4]`, target = 4: descending: after first 2: dp[2]=T. After second 2: s=4: dp[4]=dp[2](from first 2 only)=T... wait descending from target: s=4: dp[4]||dp[2]=T → dp[4]=true. But that used BOTH 2s (2+2=4)! And that's *correct* — each number used once, and there are two 2s. OK: the classic contamination example is `nums = [1, 1]` with target = 1 via *ascending within the same item* — but each outer iteration is one item, and ascending within one item: for `num=1, target=1`: s=1: dp[1]=dp[1]||dp[0]=true. Only one item processed, correct. The actual failure needs TWO copies of the same value: `nums=[1,1,1]`, target=3: ascending never reuses the *same* item because each item is one outer pass... |
223 | | - |
224 | | -Hold on — the real issue: ascending reuses the same item within one outer pass. E.g. `nums = [2]`, `target = 4`: descending: s=4: dp[4]=dp[2]=false; s=3..2: dp[2]=dp[0]=true. Result dp[4]=false ✓ (one 2 can't make 4). Ascending: s=2: dp[2]=true; s=3: dp[3]=dp[1]=false; s=4: dp[4]=dp[2]=true ← WRONG! The same single item 2 was used twice. That's the correct bug example: `canPartition([2])` with sum=2, target=1 — not applicable. Since target = sum/2 and the array must be non-empty with sum even, `[2]` gives target 1, and descending/ascending both give false. Hmm, target = sum/2 ≤ sum - min... for `[2, 2]`, sum=4, target=2: descending: after first 2: dp[2]=true. after second 2: s=2: dp[2]=dp[2]||dp[0]=true. ✓. ascending: same result. The contamination needs target > sum of all items... but target = half the sum, so if all items are < target, multiple items are needed, and within ONE item ascending can self-stack: `[2, 2, 2, 2]` sum=8 target=4. Descending: after 1st 2: dp[2]=T. 2nd: s=4: dp[4]=dp[2]=T (uses both 2s — correct). s=3,2: dp[2] stays. 3rd: s=4: dp[4] already T. ✓ true (2+2). Ascending with 1st 2: s=2: T; s=3: dp[3]=dp[1]=F; s=4: dp[4]=dp[2]=T ← TRUE but with only ONE item?! [2] can't make 4 with one copy. But wait the array has four 2s, so dp[4]=true is correct anyway. Ugh — the clean demonstration needs an array where ascending gives true but the correct answer is false. `nums=[2]` target=1 → no. `nums=[2,2]` target=2 → both true. `nums=[2,4]` sum=6 target=3 → no. `nums=[2,2,2]` sum=6 target=3 → no reachable sum 3. `nums=[2,2,2,2]`? true. `nums=[3,3]` target=3: ascending: s=3: dp[3]=dp[0]=T ✓ true. `nums=[4,4]` target=4: true. It seems for even-total arrays the ascending bug rarely flips the answer on the full array... The real flips: `nums=[2,4,6]` sum=12 target=6: descending: after 2: dp[2]; after 4: dp[4],dp[6]=dp[2] → T (2+4) ✓. ascending: after 2: dp[2],dp[3]?no, dp[4]=dp[2]→T! Wait ascending with item 2 alone reaches dp[4] via dp[2] (self-stack): dp[2]=T (s=2), s=3: dp[3]=dp[1]=F, s=4: dp[4]=dp[2]=T → "4 reachable" with only the item 2 (twice)! Then after 4: s=6: dp[6]=dp[2]=T → still true (2+4 correct). Hmm answer stays true. |
225 | | - |
226 | | -The canonical counterexample where ascending breaks the FINAL answer: need an array where target is reachable only by double-using a single item and no valid combination exists. sum even, target=sum/2. E.g. `nums=[2, 2]` — no. `nums=[1, 5]` sum=6 target=3: ascending item 1: s=1: dp[1]=T, s=2: dp[2]=dp[1]=T, s=3: dp[3]=dp[2]=T → "3 reachable" — but [1,5] can't make 3! dp[3]=true → wrong TRUE! Descending item 1: s=3: dp[3]=dp[2]=F, s=2: dp[2]=dp[1]=F, s=1: dp[1]=dp[0]=T → dp[3]=false ✓, then item 5: s=3: dp[3]=dp[3]||dp[-2] → false. Final false ✓ correct. |
227 | | - |
228 | | -So `nums=[1,5]` is the clean example. I'll use that in the dry run as the "descending vs ascending" demonstration. |
229 | | - |
230 | | -Let me fix the dry-run section in the page I'm about to write — I haven't written it yet. Good. I'll include nums=[1,5] example.<|end▁of▁thinking|> |
231 | | - |
232 | | -<||DSML||tool_calls> |
233 | | -<||DSML||invoke name="write"> |
234 | | -<||DSML||parameter name="content" string="true"># 2.6 Partition Equal Subset Sum |
235 | | - |
236 | | -> **Source:** [`src/main/kotlin/dynamic_programming/PartitionEqualSubsetSum.kt`](https://github.com/arpanpathak/AdvancedAlgorithmPatterns/blob/main/src/main/kotlin/dynamic_programming/PartitionEqualSubsetSum.kt) |
237 | | -> **Pattern:** subset-sum reachability (boolean knapsack) · **Core page** |
238 | | -
|
239 | | -## The Problem |
240 | | - |
241 | | -Given a non-empty array `nums` of positive integers, can you partition it into two subsets with **equal sums**? |
242 | | - |
243 | | -- Constraints: $1 \le n \le 200$, $1 \le nums[i] \le 100$ → total sum ≤ 20,000. |
244 | | - |
245 | | -## Examples |
246 | | - |
247 | | -``` |
248 | | -Input: nums = [1, 5, 11, 5] -> true (partition {1,5,5} and {11}; both sum to 11) |
249 | | -Input: nums = [1, 2, 3, 5] -> false (total 11 is odd, impossible) |
250 | | -Input: nums = [1, 2, 5] -> false (even total 8, but no subset sums to 4) |
251 | | -``` |
252 | | - |
253 | | -## Intuition — reduce to subset-sum |
254 | | - |
255 | | -Two observations collapse the problem: |
256 | | - |
257 | | -1. **The target is forced.** If the total sum $S$ is odd, an equal split is impossible → `false` immediately. Otherwise both halves must sum to $S/2$. |
258 | | -2. **The question becomes:** does *any* subset of `nums` sum to exactly $S/2$? (The other half is whatever's left — automatically $S/2$.) |
259 | | - |
260 | | -That's **subset-sum**, which is 0/1 knapsack with `value == weight` and a boolean question. State: |
261 | | - |
262 | | -$$ |
263 | | -dp[s] = \text{can some subset of the items seen so far sum to exactly } s |
264 | | -$$ |
265 | | - |
266 | | -Recurrence (per item `x`, descending over sums — the 0/1 discipline from [2.4](zero-one-knapsack.md)): |
267 | | - |
268 | | -$$ |
269 | | -dp[s] = dp[s] \;\lor\; dp[s - x] |
270 | | -$$ |
271 | | - |
272 | | -meaning "reachable before (skip x)" or "reachable by taking x (from state s−x)". Base: `dp[0] = true` (empty subset sums to 0). Answer: `dp[target]`. |
273 | | - |
274 | 41 | **Why descending?** Exactly the 0/1 argument: reading `dp[s - x]` in descending order guarantees it reflects *previous* items only, so each number is used at most once. The dry run below shows the catastrophic result of ascending order. |
275 | 42 |
|
276 | 43 | ## Approach 1 — Brute force |
|
0 commit comments