Builds on Linear DP, which you haven’t finished yet.
0/1 Knapsack (Partition Equal Subset Sum, Subset Sum)
You're given an integer array nums. Decide whether it can be split into two subsets whose element sums equal each other -- return true if such a split exists and false otherwise.
0/1 Knapsack
items & capacities ➔ capacity loop runs downward so each item counts once ➔ dp[w]| 1 | def can_partition(nums: list[int]) -> bool: |
| 2 | """0/1 Knapsack: Partition array into two equal subsets (reverse loop). |
| 3 | Time: O(N * Target) | Space: O(Target) |
| 4 | """ |
| 5 | total = sum(nums) |
| 6 | if total % 2 != 0: |
| 7 | return False |
| 8 | target = total // 2 |
| 9 | # dp: dp[w] is True when sum w is reachable using only the items processed so far |
| 10 | dp = [False] * (target + 1) |
| 11 | dp[0] = True |
| 12 | for x in nums: |
| 13 | for w in range(target, x - 1, -1): |
| 14 | # 1. Use This Item At Most Once: |
| 15 | # The reverse loop reads dp[w - x] before this pass updates it, so x is counted at most once per w. |
| 16 | dp[w] = dp[w] or dp[w - x] |
| 17 | return dp[target] |
x, dp[w] is True when some chosen items among those processed so far add up to exactly w.Treat each sum as its own subproblem: dp[w] is True when the items seen so far can add up to w, and each item x updates it from dp[w - x]. Loop w downward, so dp[w - x] has not been touched by the current x yet and x is counted at most once; looping upward would let the same x fill dp[w - x] and then dp[w] in one pass. The trap is exactly that loop direction: for w in range(target, x - 1, -1), not range(x, target + 1).
See it run
Partition Equal Subset Sum, step by step
x -w -dp[target] False
Row k is the list dp after the k-th item. Column w is the sum w. T means True and F means False.
writethe cell being writtenreadthe cell dp[w - x], read before this item changes itanswerthe final answer
Start: nums = [1, 5, 11, 5], total = 22, target = 11. dp[0] = True because the sum 0 needs no item. Every other cell is False.
Gotcha 1 of 3
- The trap
Gotcha 1: Looping
wupward in a 0/1 problemMistakefor w in range(x, target + 1):Fixfor w in range(target, x - 1, -1):What happens: Going upward,
dp[w - x]may already include the currentx. Thendp[w]usesxa second time. Herex = 1makes every sum from 1 to 4 reachable, so the target 4 looks reachable. Going downward,dp[w - x]still describes only the items beforex.Example: nums = [1, 2, 5]Mistake: returns TrueFix: returns False
- Each item can be used any number of times, as in Coin Change (LC 322): the capacity loop runs forward, so an item may fill
dp[w - item]and thendp[w]in the same pass. See Unbounded Knapsack - You must return every combination itself, not just whether a sum is reachable, as in Combination Sum (LC 39): a table of booleans per sum can't list them. See Combinations
Deep Dive
The state is a sum, and the items arrive one at a time
In a 0/1 knapsack problem you choose items, and each item is either taken once or left out. The state is a sum w. In can_partition, dp[w] is True when some of the items processed so far add up to exactly w.
Processing items in order is what makes the table correct. After item k, dp describes only the first k items. The next item x adds one choice: take x once, so dp[w] also becomes True when dp[w - x] was True before x.
At the start, only the sum 0 is reachable, by choosing no item. So dp[0] is True and every other cell is False. If every cell started True, every target would look reachable.
Example: for nums = [1, 5, 11, 5] the target is 11. The reachable sums are [0, 1] after the first item, [0, 1, 5, 6] after the second, [0, 1, 5, 6, 11] after 11, and [0, 1, 5, 6, 10, 11] after the last 5. The target 11 is reachable, so the answer is True.
Why the loop runs downward
Think of a table with one row per item, where dp[i][w] = dp[i - 1][w] or dp[i - 1][w - x]. Both terms read row i - 1. The code compresses the table to one array, so dp[w - x] must still hold the value from before item x.
Looping w downward gives exactly that. When the loop updates dp[w], the smaller index w - x has not changed yet in this pass. So x is used at most once.
Looping w upward does the opposite. dp[w - x] was already updated in this pass, so it may already include x, and dp[w] uses x a second time.
Example: can_partition([2, 6]) has total 8 and target 4, and the answer is False. If the loop runs upward, dp[2] becomes True, then dp[4] reads dp[2] and becomes True. The single 2 was used twice. The downward loop returns False.
Why the total must be even
Two equal halves need an even total. The code checks total % 2 != 0 first and returns False.
Without the check, total // 2 rounds down. For nums = [1, 2] the total is 3, and the rounded target is 1. The subset [1] reaches 1, so the code would return True for a split that does not exist.
The same idea turns other problems into this one. In Last Stone Weight II (LC 1049), splitting the stones into two groups with the smallest weight difference means finding the largest reachable sum that is at most half of the total. For [2, 7, 4, 1, 8, 1] the answer is 1. In Target Sum (LC 494), choosing plus or minus signs turns into counting the subsets that add up to (total + target) / 2. The table then holds counts, and dp[0] starts at 1.
The cost: states times work per state, and why it is pseudo-polynomial
There are N items and target sums, and each update is one comparison. The time is O(N * Target). The space is O(Target) for the single array.
The cost depends on the value of target, not on how many digits it has. A target of 10^9 would need a billion-entry array. This is called pseudo-polynomial time: it is polynomial in the number's value but exponential in the number of bits that write it. So check the constraints first. In Partition Equal Subset Sum (LC 416), the total is at most 20,000.
A second limit can be a pair. In Ones and Zeroes (LC 474), each string uses some zeros and some ones, and two budgets limit the choice. The state becomes the pair (zeros, ones), and both loops run downward. The time is O(N * M * K) for M zeros and K ones. For ["10", "0001", "111001", "1", "0"] with 5 zeros and 3 ones, the answer is 4.
60-Second Pitch
"The brute force tries every subset of the items, which is 2 to the power N subsets. For an equal split, the total must be even, and I look for a subset that adds up to half of it. I define dp[w] as True when some of the items seen so far add up to w. Only dp[0] starts True. Each item x sets dp[w] from dp[w - x]. I loop w downward, so dp[w - x] has not been touched by this item, and the item is used at most once. There are N items and up to total over 2 sums, so the time is O(N * Target). The space is O(Target). The time depends on the value of the target, so I check that the target is small before I choose this method."
Common Questions
- Why is the 0/1 knapsack loop downward?
The one-dimensional array holds one row of the table. The update must read the row before the current item. A downward loop reaches the smaller index
w - xbefore it is changed in this pass, so it still holds the old value. An upward loop would read a value that may already includex.- What are the time and space complexity, and is it polynomial?
O(N * Target) time and O(Target) space. It is pseudo-polynomial: it grows with the value of
target, not with the number of digits oftarget. For a target near 10^9, the array would not fit in memory.- How do you recover which items were chosen?
A one-dimensional array loses that information. Keep the two-dimensional table
dp[i][w]and walk back from the last row. Ifdp[i][w]equalsdp[i - 1][w], itemiwas skipped. Otherwise itemiwas taken, andwdecreases by its size. This costs O(N * Target) space.- How would you count the subsets instead of asking whether one exists?
Make
dphold counts, withdp[0] = 1anddp[w] += dp[w - x]in the same downward loop. This is the step used in Target Sum (LC 494) after the signs are turned into a subset sum.- How would you handle a very large target?
First check whether the numbers have more structure. If the number of items is small, about 40 or fewer, a meet-in-the-middle search can work. It lists the subset sums of each half and combines them. If the target is large but the items are small, the table can sometimes use a bitset, where one bit stands for each reachable sum. Say which constraint you rely on.
SWE Reasoning
Spot it
- You choose items, and each item is taken at most once. The chosen sizes must add up to a target, or stay within a budget.
- The question is yes or no, a count of subsets, or the best value: can the target be made, how many subsets, how large a sum.
- The target is small enough to be an array index, usually below about 20,000.
- A problem about two equal groups, or the smallest difference between two groups, can be reduced to a subset sum of half the total.
- The statement says each item is used once. That picks the downward loop.
The invariant, step by step
- The total is checked first. If it is odd, the code returns False. Otherwise
target = total // 2. dp[0]is True and every other cell is False. Only the sum 0 is reachable before any item.- The outer loop takes one item
x. At this momentdp[w]is correct for all items before it. - The inner loop visits each sum
wfromtargetdown tox. - The update
dp[w] = dp[w] or dp[w - x]readsdp[w - x], which the downward loop has not changed yet in this pass. Soxis counted at most once. - When the outer loop ends,
dp[w]is True when some chosen items add up to exactlyw, which is the card's invariant. The code returnsdp[target].
Traps
- Loop upward. One item is used more than once.
can_partition([1, 2, 5])returns True instead of False. Withx = 1, every sum from 1 to 4 looks reachable. - Skip the odd-total check.
total // 2rounds down, and the search finds a subset for the smaller target.can_partition([1, 2])returns True instead of False. - Start
dpwith every sum reachable. If every cell starts True, every target looks reachable.can_partition([1, 2, 5])returns True instead of False. - Forget
dp[0] = True. Then no sum is ever reachable, and the answer is always False. - Use a huge target. An array of size
target + 1is too large when the target is around 10^9. Check the constraint before choosing this method.
Practice
Builds on Linear DP, which you haven’t finished yet.
0/1 Knapsack (Partition Equal Subset Sum, Subset Sum)
You're given an integer array nums. Decide whether it can be split into two subsets whose element sums equal each other -- return true if such a split exists and false otherwise.
0/1 Knapsack
items & capacities ➔ capacity loop runs downward so each item counts once ➔ dp[w]| 1 | def can_partition(nums: list[int]) -> bool: |
| 2 | """0/1 Knapsack: Partition array into two equal subsets (reverse loop). |
| 3 | Time: O(N * Target) | Space: O(Target) |
| 4 | """ |
| 5 | total = sum(nums) |
| 6 | if total % 2 != 0: |
| 7 | return False |
| 8 | target = total // 2 |
| 9 | # dp: dp[w] is True when sum w is reachable using only the items processed so far |
| 10 | dp = [False] * (target + 1) |
| 11 | dp[0] = True |
| 12 | for x in nums: |
| 13 | for w in range(target, x - 1, -1): |
| 14 | # 1. Use This Item At Most Once: |
| 15 | # The reverse loop reads dp[w - x] before this pass updates it, so x is counted at most once per w. |
| 16 | dp[w] = dp[w] or dp[w - x] |
| 17 | return dp[target] |
x, dp[w] is True when some chosen items among those processed so far add up to exactly w.Treat each sum as its own subproblem: dp[w] is True when the items seen so far can add up to w, and each item x updates it from dp[w - x]. Loop w downward, so dp[w - x] has not been touched by the current x yet and x is counted at most once; looping upward would let the same x fill dp[w - x] and then dp[w] in one pass. The trap is exactly that loop direction: for w in range(target, x - 1, -1), not range(x, target + 1).
See it run
Partition Equal Subset Sum, step by step
x -w -dp[target] False
Row k is the list dp after the k-th item. Column w is the sum w. T means True and F means False.
writethe cell being writtenreadthe cell dp[w - x], read before this item changes itanswerthe final answer
Start: nums = [1, 5, 11, 5], total = 22, target = 11. dp[0] = True because the sum 0 needs no item. Every other cell is False.
Gotcha 1 of 3
- The trap
Gotcha 1: Looping
wupward in a 0/1 problemMistakefor w in range(x, target + 1):Fixfor w in range(target, x - 1, -1):What happens: Going upward,
dp[w - x]may already include the currentx. Thendp[w]usesxa second time. Herex = 1makes every sum from 1 to 4 reachable, so the target 4 looks reachable. Going downward,dp[w - x]still describes only the items beforex.Example: nums = [1, 2, 5]Mistake: returns TrueFix: returns False
- Each item can be used any number of times, as in Coin Change (LC 322): the capacity loop runs forward, so an item may fill
dp[w - item]and thendp[w]in the same pass. See Unbounded Knapsack - You must return every combination itself, not just whether a sum is reachable, as in Combination Sum (LC 39): a table of booleans per sum can't list them. See Combinations
Deep Dive
The state is a sum, and the items arrive one at a time
In a 0/1 knapsack problem you choose items, and each item is either taken once or left out. The state is a sum w. In can_partition, dp[w] is True when some of the items processed so far add up to exactly w.
Processing items in order is what makes the table correct. After item k, dp describes only the first k items. The next item x adds one choice: take x once, so dp[w] also becomes True when dp[w - x] was True before x.
At the start, only the sum 0 is reachable, by choosing no item. So dp[0] is True and every other cell is False. If every cell started True, every target would look reachable.
Example: for nums = [1, 5, 11, 5] the target is 11. The reachable sums are [0, 1] after the first item, [0, 1, 5, 6] after the second, [0, 1, 5, 6, 11] after 11, and [0, 1, 5, 6, 10, 11] after the last 5. The target 11 is reachable, so the answer is True.
Why the loop runs downward
Think of a table with one row per item, where dp[i][w] = dp[i - 1][w] or dp[i - 1][w - x]. Both terms read row i - 1. The code compresses the table to one array, so dp[w - x] must still hold the value from before item x.
Looping w downward gives exactly that. When the loop updates dp[w], the smaller index w - x has not changed yet in this pass. So x is used at most once.
Looping w upward does the opposite. dp[w - x] was already updated in this pass, so it may already include x, and dp[w] uses x a second time.
Example: can_partition([2, 6]) has total 8 and target 4, and the answer is False. If the loop runs upward, dp[2] becomes True, then dp[4] reads dp[2] and becomes True. The single 2 was used twice. The downward loop returns False.
Why the total must be even
Two equal halves need an even total. The code checks total % 2 != 0 first and returns False.
Without the check, total // 2 rounds down. For nums = [1, 2] the total is 3, and the rounded target is 1. The subset [1] reaches 1, so the code would return True for a split that does not exist.
The same idea turns other problems into this one. In Last Stone Weight II (LC 1049), splitting the stones into two groups with the smallest weight difference means finding the largest reachable sum that is at most half of the total. For [2, 7, 4, 1, 8, 1] the answer is 1. In Target Sum (LC 494), choosing plus or minus signs turns into counting the subsets that add up to (total + target) / 2. The table then holds counts, and dp[0] starts at 1.
The cost: states times work per state, and why it is pseudo-polynomial
There are N items and target sums, and each update is one comparison. The time is O(N * Target). The space is O(Target) for the single array.
The cost depends on the value of target, not on how many digits it has. A target of 10^9 would need a billion-entry array. This is called pseudo-polynomial time: it is polynomial in the number's value but exponential in the number of bits that write it. So check the constraints first. In Partition Equal Subset Sum (LC 416), the total is at most 20,000.
A second limit can be a pair. In Ones and Zeroes (LC 474), each string uses some zeros and some ones, and two budgets limit the choice. The state becomes the pair (zeros, ones), and both loops run downward. The time is O(N * M * K) for M zeros and K ones. For ["10", "0001", "111001", "1", "0"] with 5 zeros and 3 ones, the answer is 4.
60-Second Pitch
"The brute force tries every subset of the items, which is 2 to the power N subsets. For an equal split, the total must be even, and I look for a subset that adds up to half of it. I define dp[w] as True when some of the items seen so far add up to w. Only dp[0] starts True. Each item x sets dp[w] from dp[w - x]. I loop w downward, so dp[w - x] has not been touched by this item, and the item is used at most once. There are N items and up to total over 2 sums, so the time is O(N * Target). The space is O(Target). The time depends on the value of the target, so I check that the target is small before I choose this method."
Common Questions
- Why is the 0/1 knapsack loop downward?
The one-dimensional array holds one row of the table. The update must read the row before the current item. A downward loop reaches the smaller index
w - xbefore it is changed in this pass, so it still holds the old value. An upward loop would read a value that may already includex.- What are the time and space complexity, and is it polynomial?
O(N * Target) time and O(Target) space. It is pseudo-polynomial: it grows with the value of
target, not with the number of digits oftarget. For a target near 10^9, the array would not fit in memory.- How do you recover which items were chosen?
A one-dimensional array loses that information. Keep the two-dimensional table
dp[i][w]and walk back from the last row. Ifdp[i][w]equalsdp[i - 1][w], itemiwas skipped. Otherwise itemiwas taken, andwdecreases by its size. This costs O(N * Target) space.- How would you count the subsets instead of asking whether one exists?
Make
dphold counts, withdp[0] = 1anddp[w] += dp[w - x]in the same downward loop. This is the step used in Target Sum (LC 494) after the signs are turned into a subset sum.- How would you handle a very large target?
First check whether the numbers have more structure. If the number of items is small, about 40 or fewer, a meet-in-the-middle search can work. It lists the subset sums of each half and combines them. If the target is large but the items are small, the table can sometimes use a bitset, where one bit stands for each reachable sum. Say which constraint you rely on.
SWE Reasoning
Spot it
- You choose items, and each item is taken at most once. The chosen sizes must add up to a target, or stay within a budget.
- The question is yes or no, a count of subsets, or the best value: can the target be made, how many subsets, how large a sum.
- The target is small enough to be an array index, usually below about 20,000.
- A problem about two equal groups, or the smallest difference between two groups, can be reduced to a subset sum of half the total.
- The statement says each item is used once. That picks the downward loop.
The invariant, step by step
- The total is checked first. If it is odd, the code returns False. Otherwise
target = total // 2. dp[0]is True and every other cell is False. Only the sum 0 is reachable before any item.- The outer loop takes one item
x. At this momentdp[w]is correct for all items before it. - The inner loop visits each sum
wfromtargetdown tox. - The update
dp[w] = dp[w] or dp[w - x]readsdp[w - x], which the downward loop has not changed yet in this pass. Soxis counted at most once. - When the outer loop ends,
dp[w]is True when some chosen items add up to exactlyw, which is the card's invariant. The code returnsdp[target].
Traps
- Loop upward. One item is used more than once.
can_partition([1, 2, 5])returns True instead of False. Withx = 1, every sum from 1 to 4 looks reachable. - Skip the odd-total check.
total // 2rounds down, and the search finds a subset for the smaller target.can_partition([1, 2])returns True instead of False. - Start
dpwith every sum reachable. If every cell starts True, every target looks reachable.can_partition([1, 2, 5])returns True instead of False. - Forget
dp[0] = True. Then no sum is ever reachable, and the answer is always False. - Use a huge target. An array of size
target + 1is too large when the target is around 10^9. Check the constraint before choosing this method.