Knapsack DP
Scope โ The knapsack family in full: 0/1 vs unbounded vs bounded vs group, the subset-sum reduction, why the 0/1 inner loop runs backward, and the loop-order rule that separates combinations from permutations. See also: dp.md โ the one-screen knapsack template and the rest of the DP patterns; knapsack_01_zh.md โ 0/1 ่ๅ ็ไธญๆ่ฉณ่งฃ โ a Traditional Chinese walkthrough of the 0/1 case only; combinatorics_math_patterns.md โ counting without DP.
LeetCode Problem Lists
Overview
Key Properties
- Complexity:
O(n * W)time,O(W)space after the 1-D rollup โnitems,Wcapacity/target. - Core Idea: every item is a take / skip decision (a pick-one-of-many decision once an item carries a menu of options), and the DP dimension that separates the variants is the capacity axis โ whether the inner loop reads values that already include the current item.
- When to Use: a fixed set of items each with a cost, a hard capacity/target, and a max / feasibility / count-the-ways question over subsets.
The one table that decides everything
| Variant | Reuse | Outer loop | Inner loop | LC |
|---|---|---|---|---|
| 0/1 | each item โค 1 time | items | capacity, backward | 416, 494, 1049, 474 |
| Unbounded โ combinations | unlimited, order doesnโt matter | items | amount, forward | 518 |
| Unbounded โ permutations | unlimited, order does matter | amount | items | 377 |
| Unbounded โ min/max | unlimited, order irrelevant | either | either | 322, 279, 1449 |
| Bounded | each item โค k times |
items (binary-split into 0/1 copies) | capacity, backward | 2585, 1774 |
| Group | โค 1 option of each item | items | capacity backward, outer; options inner | 4040, 1155, 2218 |
References
- dp.md โ the short knapsack template and the rest of the DP pattern family
- knapsack_01_zh.md โ 0/1 ่ๅ ไธญๆ่ฉณ่งฃ๏ผstate ๅฎ็พฉใๅๅบ traceใLC 494/416 ่งฃ้กๆต็จ
- Knapsack problem โ Wikipedia
Problem Categories
| Category | Question it answers | Answer type | LC |
|---|---|---|---|
| Subset feasibility | can some subset hit exactly this sum? | boolean | 416, 1049, 2915 |
| Subset counting | how many subsets hit it? | int (ways) | 494, 518 |
| Best value under a cap | most value that fits the capacity? | int (max) | classic 0/1, 474, 879 |
| Fewest items to a target | min coins / squares to make the amount? | int (min) or -1 | 322, 279 |
| Ordered vs unordered counting | is 1+2 the same as 2+1? |
decides the loop nesting | 518 vs 377 |
| One form per item | each item may be used in one of several forms โ which? | int (min cost) | 4040, 1155, 2218 |
Templates & Algorithms
Loop Order: Combinations vs Permutations
๐ Key Insight: In unbounded knapsack problems (like Coin Change), the order of nested loops determines whether you count combinations or permutations.
Both orders here are correct DPs answering different questions โ that is special to sums. When the items are concatenated rather than added (LC 139 Word Break), the item-outer order is not a different question, it is simply wrong: dp_loop_order.md.
๐ฏ Ultimate Cheat Sheet: When to Use Which Pattern
| When Problem Saysโฆ | Pattern to Use | Loop Order | Direction | DP Transition | Example LC |
|---|---|---|---|---|---|
| โCount waysโ + order doesnโt matter | Combinations | Item โ Target | Forward | dp[i] += dp[i-item] |
518 |
| โCount waysโ + order matters | Permutations | Target โ Item | Forward | dp[i] += dp[i-item] |
377 |
| โUse each item onceโ + find max/min | 0/1 Knapsack | Item โ Capacity | Backward | dp[w] = max(dp[w], ...) |
416 |
| โUnlimited itemsโ + find max/min | Unbounded Knapsack | Item โ Capacity | Forward | dp[i] = min(dp[i], ...) |
322 |
โก Quick Recognition (่ฏๅซ):
- See โdifferent sequencesโ or โdifferent orderingsโ โ Permutations (Target outer)
- See โnumber of combinationsโ or โunique waysโ โ Combinations (Item outer)
- See โeach element at most onceโ โ 0/1 Knapsack (Backward)
- See โminimum coinsโ or โfewest itemsโ โ Unbounded Knapsack (Forward)
๐ Visual Summary: The Four Core Patterns
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ DP KNAPSACK PATTERN MATRIX โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
COUNT WAYS FIND MIN/MAX
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ โ
ORDER MATTERS? โ PERMUTATIONS โ Not typically used โ
(Yes) โ LC 377 โ (Use Permutations โ
โ TargetโItem โ for counting) โ
โ Forward โ โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ
ORDER DOESN'T โ COMBINATIONS โ UNBOUNDED KNAPSACK โ
MATTER โ LC 518 โ LC 322 โ
(No) โ ItemโTarget โ ItemโCapacity โ
โ Forward โ Forward โ
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ โ โ
USE EACH ONCE โ Not typical โ 0/1 KNAPSACK โ
(Constraint) โ (Can adapt โ LC 416 โ
โ 0/1 pattern) โ ItemโCapacity โ
โ โ BACKWARD โ ๏ธ โ
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Legend:
ItemโTarget = Outer loop: items, Inner loop: target
TargetโItem = Outer loop: target, Inner loop: items
Forward = Inner loop: i to target (allows reuse)
BACKWARD โ ๏ธ = Inner loop: target to i (prevents reuse)
๐ฏ Decision Flow:
Start
โ
โโ Question asks "count ways"?
โ โ
โ โโ YES โ Order matters?
โ โ โโ YES โ Permutations (TargetโItem) [LC 377]
โ โ โโ NO โ Combinations (ItemโTarget) [LC 518]
โ โ
โ โโ NO โ Question asks "min/max"?
โ โ
โ โโ Each item once?
โ โ โโ YES โ 0/1 Knapsack (BACKWARD) [LC 416]
โ โ โโ NO โ Unbounded (FORWARD) [LC 322]
โ โ
โ โโ Unknown โ Check problem constraints
๐ Master Pattern Table: DP Transitions by Problem Type
| Pattern Type | Loop Order | DP Transition | What It Counts | Mental Model | Example | Result |
|---|---|---|---|---|---|---|
| COMBINATIONS (Order doesnโt matter) |
ITEM โ TARGETfor item in items:ย ย for i in range(item, target+1): |
dp[i] += dp[i - item] |
Unique sets [1,2] = [2,1] |
โProcess all uses of item-1, then all uses of item-2โ Forces canonical order |
LC 518 coins=[1,2] amount=3 |
2 ways {1,1,1} {1,2} |
| PERMUTATIONS (Order matters) |
TARGET โ ITEMfor i in range(1, target+1):ย ย for item in items: |
dp[i] += dp[i - item] |
Different orderings [1,2] โ [2,1] |
โFor each target, try every item as the โlastโ oneโ Allows any order |
LC 377 nums=[1,2] target=3 |
3 ways {1,1,1} {1,2} {2,1} |
| 0/1 KNAPSACK (Use each once) |
ITEM โ CAPACITY (backwards) for item in items:ย ย for w in range(W, weight-1, -1): |
dp[w] = max(dp[w],dp[w-weight[i]] + value[i]) |
Max/min with constraint Each item used โค 1 time |
โMust iterate backwards to avoid using same item twice in one passโ | LC 416 Partition Subset |
True/False or Max value |
| UNBOUNDED KNAPSACK (Unlimited use) |
ITEM โ CAPACITY (forwards) for item in items:ย ย for w in range(weight, W+1): |
dp[w] = max(dp[w],dp[w-weight[i]] + value[i]) |
Max/min without constraint Each item used unlimited |
โIterate forwards - can use updated values in same passโ | LC 322 Coin Change (min coins) |
Min count or -1 |
๐ป Code Templates by Pattern
// java
// IDEA: the four knapsack loop orders side by side โ each differs only in nesting/direction
// time = O(n * W), space = O(W)
// ============================================
// PATTERN 1: COMBINATIONS (Item โ Target)
// ============================================
// LC 518: Coin Change II
public int countCombinations(int target, int[] items) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: one way to make 0
// OUTER: Items/Coins
for (int item : items) {
// INNER: Target/Amount (forward)
for (int i = item; i <= target; i++) {
dp[i] += dp[i - item]; // โ Same transition
}
}
return dp[target];
}
// ============================================
// PATTERN 2: PERMUTATIONS (Target โ Item)
// ============================================
// LC 377: Combination Sum IV
public int countPermutations(int target, int[] items) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: one way to make 0
// OUTER: Target/Amount
for (int i = 1; i <= target; i++) {
// INNER: Items/Coins
for (int item : items) {
if (i >= item) {
dp[i] += dp[i - item]; // โ Same transition
}
}
}
return dp[target];
}
// ============================================
// PATTERN 3: 0/1 KNAPSACK (Item โ Capacity BACKWARDS)
// ============================================
// LC 416: Partition Equal Subset Sum
public boolean canPartition(int[] nums, int target) {
boolean[] dp = new boolean[target + 1];
dp[0] = true; // Base: can make 0
// OUTER: Items
for (int num : nums) {
// INNER: Capacity (BACKWARDS to prevent reuse)
for (int w = target; w >= num; w--) {
dp[w] = dp[w] || dp[w - num]; // โ Different transition (OR)
}
}
return dp[target];
}
// ============================================
// PATTERN 4: UNBOUNDED KNAPSACK (Item โ Capacity FORWARDS)
// ============================================
// LC 322: Coin Change (minimum coins)
public int minCoins(int target, int[] coins) {
int[] dp = new int[target + 1];
Arrays.fill(dp, target + 1); // Infinity
dp[0] = 0; // Base: 0 coins for 0 amount
// OUTER: Items/Coins
for (int coin : coins) {
// INNER: Target (FORWARDS allows reuse)
for (int i = coin; i <= target; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1); // โ Different transition (MIN)
}
}
return dp[target] > target ? -1 : dp[target];
}
๐ Key Observations:
-
Same DP Transition (
dp[i] += dp[i - item]) for:- Combinations (Item โ Target)
- Permutations (Target โ Item)
- Only difference: Loop order!
-
Different DP Transitions for:
- 0/1 Knapsack:
dp[w] = dp[w] || dp[w - num](boolean OR or MAX) - Unbounded Knapsack:
dp[i] = min(dp[i], dp[i - coin] + 1)(MIN/MAX)
- 0/1 Knapsack:
-
Direction Matters for knapsack:
- Backwards โ prevents reuse (0/1)
- Forwards โ allows reuse (unbounded)
๐ฏ Pattern Selection Decision Tree
Question: What does the problem ask for?
โโ "Count number of ways/combinations to reach target"
โ โโ Order matters? (e.g., [1,2] โ [2,1])
โ โ โโ YES โ Use PERMUTATIONS pattern (Target โ Item)
โ โ โ Example: LC 377 Combination Sum IV
โ โ โโ NO โ Use COMBINATIONS pattern (Item โ Target)
โ โ Example: LC 518 Coin Change II
โ โ
โ โโ Can reuse items?
โ โโ YES โ Unbounded, iterate forwards
โ โโ NO โ 0/1 Knapsack, iterate backwards
โ
โโ "Find minimum/maximum value"
โโ Can reuse items?
โ โโ YES โ Unbounded Knapsack (forwards)
โ โ Example: LC 322 Coin Change (min coins)
โ โโ NO โ 0/1 Knapsack (backwards)
โ Example: LC 416 Partition Equal Subset Sum
โ
โโ Always use (Item โ Capacity) order
Deep Dive: 0/1 Knapsack & Subset Sum Pattern ๐
This pattern is fundamental and appears in many disguised forms. Last Stone Weight II is a great example of recognizing when a problem is secretly a subset sum problem.
When to Use This Pattern
Use 0/1 Knapsack / Subset Sum when you see:
| Indicator | What It Means | Example |
|---|---|---|
| โPartitionโ or โsplit into two groupsโ | Divide items into subsets | LC 1049 (Last Stone Weight II) |
| โMaximize/minimize the differenceโ | Find optimal partition | LC 1049, 494 |
| โCan you achieve sum X?โ | Check if specific sum possible | LC 416 (Equal Subset Partition) |
| โEach item used at most onceโ | 0/1 constraint (not unlimited) | All of above |
| โMinimize difference between groupsโ | Partition into balanced groups | LC 1049 |
Key Recognition: If you see โpartitionโ or โdivide into two groupsโ โ think 0/1 Knapsack.
Core Idea: The Mathematical Transformation ๐งฎ
Problem: Partition array into two groups and minimize difference.
Given: stones = [2, 7, 4, 1, 8, 1]
Total sum = 23
Goal: Split into two groups with min |sum1 - sum2|
Mathematical insight:
Let sum1 = S (sum of group 1)
Then sum2 = total - S (sum of group 2)
Difference = |sum1 - sum2| = |S - (total - S)| = |2S - total|
To minimize this: Maximize S such that S โค total/2
Result = total - 2*S (where S is the largest achievable sum โค total/2)
Why This Works:
- Find the largest subset sum that doesnโt exceed
total / 2 - This gives the most balanced partition possible
- The remaining group has sum =
total - S - Their difference =
(total - S) - S = total - 2*S
Pattern: Two Variants
Variant 1: Boolean DP (Can we achieve this sum?)
// java
// LC 1049 - Last Stone Weight II
// IDEA: variant 1 โ boolean subset sum; can we reach exactly `sum`?
// time = O(n * total), space = O(total)
public int lastStoneWeightII(int[] stones) {
int total = 0;
for (int stone : stones) {
total += stone;
}
int target = total / 2;
// dp[j] = can we achieve sum j?
boolean[] dp = new boolean[target + 1];
dp[0] = true; // Base: always can make sum 0 (choose nothing)
// For each stone
for (int stone : stones) {
// Iterate BACKWARDS to prevent using same stone twice
for (int j = target; j >= stone; j--) {
dp[j] = dp[j] || dp[j - stone]; // Can achieve j if:
// (already could) OR (could make j-stone and add this stone)
}
}
// Find largest achievable sum โค target
for (int j = target; j >= 0; j--) {
if (dp[j]) {
return total - 2 * j;
}
}
return 0;
}
Variant 2: Integer DP (Maximum value achievable)
// java
// LC 1049 - Last Stone Weight II
// IDEA: variant 2 โ maximise the achievable subset sum <= total/2, answer = total - 2*best
// time = O(n * total), space = O(total)
public int lastStoneWeightII(int[] stones) {
int total = 0;
for (int stone : stones) {
total += stone;
}
int target = total / 2;
// dp[j] = maximum sum we can achieve โค j
int[] dp = new int[target + 1];
dp[0] = 0; // Base: can make sum 0
// For each stone
for (int stone : stones) {
// Iterate BACKWARDS to prevent reuse
for (int j = target; j >= stone; j--) {
// Either skip this stone (dp[j])
// Or include it and add to best we could do with j-stone (dp[j-stone] + stone)
dp[j] = Math.max(dp[j], dp[j - stone] + stone);
}
}
return total - 2 * dp[target];
}
Why Iterate BACKWARDS? (The Critical Detail)
โ WRONG: Forward iteration (causes reuse)
for (int j = stone; j <= target; j++) {
dp[j] = dp[j] || dp[j - stone];
}
Problem: When we update dp[j], we're using the NEW value of dp[j-stone]
which might have already been updated by the same stone in this iteration.
This allows using the same stone multiple times!
Example with stone=3, target=9:
j=3: dp[3] = dp[0] = true โ
j=6: dp[6] = dp[3] = true โ BUT dp[3] was just updated by the same stone!
j=9: dp[9] = dp[6] = true โ Again, using same stone multiple times!
โ
CORRECT: Backward iteration (prevents reuse)
for (int j = target; j >= stone; j--) {
dp[j] = dp[j] || dp[j - stone];
}
Reason: We process from right to left, so dp[j-stone] is always from the PREVIOUS iteration
(before this stone was considered). So we use each stone only once.
Example with stone=3, target=9:
j=9: dp[9] = dp[6] (old value from previous stone) โ
j=6: dp[6] = dp[3] (old value from previous stone) โ
j=3: dp[3] = dp[0] (old value from previous stone) โ
Complete Example: Last Stone Weight II
stones = [2, 7, 4, 1, 8, 1]
total = 23
target = 23 / 2 = 11
Initial: dp = [T, F, F, F, F, F, F, F, F, F, F, F]
After stone 2:
dp[2] = T (can make sum 2)
dp = [T, F, T, F, F, F, F, F, F, F, F, F]
After stone 7:
dp[9] = T (can make 2+7)
dp[7] = T
dp[2] = T (unchanged)
dp = [T, F, T, F, F, F, F, T, F, T, F, F]
After stone 4:
dp[11] = T (can make 7+4)
dp[9] = T (unchanged)
dp[6] = T (can make 2+4)
dp[4] = T
dp = [T, F, T, F, T, F, T, T, F, T, F, T]
... continue for remaining stones ...
Final: Find largest j โค 11 where dp[j] = T
Result = 23 - 2 * j
Similar LeetCode Problems ๐
| Problem | Goal | Transformation | Complexity |
|---|---|---|---|
| LC 1049: Last Stone II | Min weight of last stone | Partition into two groups, minimize difference | O(n ร sum/2) |
| LC 416: Partition Equal Subset | Can partition into equal sums? | Can achieve sum = total/2? | O(n ร sum/2) |
| LC 494: Target Sum | Count ways to reach target | Treat as: group(+) sum1, group(-) sum2; solve sum1 - sum2 = target | O(n ร sum) |
| LC 879: Profitable Schemes | Count valid profit schemes | DP on (company count, profit) | O(n ร k ร p) |
Transformation Examples:
LC 416 (Partition Equal Subset):
Question: Can we partition into two equal subsets?
Answer: Can we achieve sum = total/2?
DP: boolean[] dp where dp[j] = can we make sum j?
Return: dp[total/2]
LC 494 (Target Sum):
Question: Assign +/- to reach target T
Transformation: Let sum1 = sum of items with +
Let sum2 = sum of items with -
sum1 - sum2 = T
sum1 + sum2 = total (all items)
Solving: sum1 = (total + T) / 2
Feasibility first (both are required before the DP runs):
abs(T) > total -> 0 ways: even all-plus or all-minus cannot reach T
(total + T) is odd -> 0 ways: sum1 would not be an integer
So: This is 0/1 knapsack! Find count of subsets with sum = (total + T) / 2
DP: int[] dp where dp[j] = count of ways to make sum j
Return: dp[(total + T) / 2]
// java
// LC 494 - Target Sum
// IDEA: reduce "assign +/-" to "count subsets summing to (total + T) / 2", then 0/1 knapsack
// time = O(n * target), space = O(target)
public int findTargetSumWays(int[] nums, int target) {
int total = 0;
for (int x : nums) total += x;
// NOTE !!! guard before the division โ otherwise `sub` is negative or non-integral
if (Math.abs(target) > total || ((total + target) % 2) != 0) return 0;
int sub = (total + target) / 2;
int[] dp = new int[sub + 1];
dp[0] = 1; // one way to make 0: pick nothing
for (int num : nums) {
for (int j = sub; j >= num; j--) { // backward -> each num used at most once
dp[j] += dp[j - num];
}
}
return dp[sub];
}
Common Pitfalls โ ๏ธ
-
Iterating forwards instead of backwards
- Will allow reusing same item multiple times
- Use backwards iteration for 0/1 knapsack
-
Wrong DP transition
- For boolean:
dp[j] = dp[j] || dp[j - weight] - For integer sum:
dp[j] = Math.max(dp[j], dp[j - weight] + weight) - For counting ways:
dp[j] += dp[j - weight] - Donโt mix these up!
- For boolean:
-
Not recognizing the โpartitionโ pattern
- โDifference between groupsโ โ Think partition
- โSplit into two teamsโ โ Think partition
- โDivide arrayโ โ Think partition
-
Integer overflow with sum
- When total sum is large, watch for overflow
- Consider using long if needed
โก Quick Reference: Loop Order โ Problem Type
| Outer Loop | Inner Loop | Pattern Name | Use When | Problems |
|---|---|---|---|---|
| Items/Coins | Target/Amount | Combinations | Count unique sets (order doesnโt matter) | LC 518 |
| Target/Amount | Items/Coins | Permutations | Count sequences (order matters) | LC 377 |
| Items (backwards) | Capacity | 0/1 Knapsack | Each item used once, find max/min | LC 416, 494 |
| Items (forwards) | Capacity | Unbounded Knapsack | Unlimited items, find max/min | LC 322 |
Quick Comparison Table
| Aspect | Combinations (LC 518) | Permutations (LC 377) |
|---|---|---|
| Loop Order | Coin โ Amount | Amount โ Coin |
| Order Matters? | โ No: [1,2] = [2,1] | โ Yes: [1,2] โ [2,1] |
| Problem Type | Coin Change II | Combination Sum IV |
| Outer Loop | for (int coin : coins) |
for (int i = 1; i <= target; i++) |
| Inner Loop | for (int i = coin; i <= amount; i++) |
for (int num : nums) |
| Example | amount=3, coins=[1,2] โ 2 ways | target=3, nums=[1,2] โ 3 ways |
Pattern 1: Combinations (Outer: Coins, Inner: Amount)
// java
// IDEA: coins outer, amount inner -> each coin is offered once, so sets are counted
// time = O(n * amount), space = O(amount)
// LC 518: Coin Change II - Count combinations
// Example: [1,2] and [2,1] are the SAME combination
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1; // Base case: 1 way to make amount 0
// OUTER LOOP: Iterate through each coin
// This ensures we process all uses of one coin before moving to the next,
// which prevents duplicate combinations like [1,2] and [2,1].
for (int coin : coins) {
// INNER LOOP: Update dp table for all amounts reachable by this coin
for (int i = coin; i <= amount; i++) {
// Number of ways to make amount 'i' is:
// (Current ways) + (Ways to make 'i - coin')
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
Why This Works:
- Process coins one at a time (e.g., first all 1s, then all 2s, then all 5s)
- By the time you use coin
2, youโve finished all calculations with coin1 - Impossible to place a
1after a2, forcing non-decreasing order - Result: Only combinations (order doesnโt matter)
Example Trace: coins = [1,2], amount = 3
After coin 1: dp = [1, 1, 1, 1] // {}, {1}, {1,1}, {1,1,1}
After coin 2: dp = [1, 1, 2, 2] // + {2}, {1,2}
Result: 2 combinations โ {1,1,1}, {1,2}
Pattern 2: Permutations (Outer: Amount, Inner: Coins)
// java
// IDEA: amount outer, coins inner -> every coin is retried at every amount, so orderings count
// time = O(n * target), space = O(target)
// LC 377: Combination Sum IV - Count permutations
// Example: [1,2] and [2,1] are DIFFERENT permutations
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
// OUTER LOOP: Iterate through each amount
// For each amount, try all coins to see which was "last added"
for (int i = 1; i <= target; i++) {
// INNER LOOP: Try each coin for current amount
for (int num : nums) {
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
Why This Counts Permutations:
- For each amount, ask: โWhat was the last coin I added?โ
- Every coin can be the โlastโ coin at each step
- Result: Permutations (order matters)
Example Trace: nums = [1,2], target = 3
dp[1]: Use 1 โ [1] (1 way)
dp[2]: Use 1 โ [1,1], Use 2 โ [2] (2 ways)
dp[3]: From dp[2] add 1 โ [1,1,1], [2,1]
From dp[1] add 2 โ [1,2]
Result: 3 permutations โ {1,1,1}, {1,2}, {2,1}
Comparison Table
| Loop Order | Result Type | Problem Example | Use Case |
|---|---|---|---|
| Outer: Coin Inner: Amount |
Combinations (Order doesnโt matter) |
LC 518 Coin Change II | Count unique coin combinations |
| Outer: Amount Inner: Coin |
Permutations (Order matters) |
LC 377 Combination Sum IV | Count different orderings |
๐ฅ Side-by-Side Code Comparison
LC 518: Coin Change II (Combinations)
// java
// LC 518 - Coin Change II
// IDEA: combinations โ coins outer
// time = O(n * amount), space = O(amount)
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1; // Base: 1 way to make 0
// CRITICAL: Coin outer loop = COMBINATIONS
for (int coin : coins) { // โ Process coins one by one
for (int i = coin; i <= amount; i++) { // โ Update all amounts for this coin
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
// Example: amount=3, coins=[1,2]
// Result: 2 combinations
// {1,1,1}, {1,2} (Note: [1,2] and [2,1] counted as same)
LC 377: Combination Sum IV (Permutations)
// java
// LC 377 - Combination Sum IV
// IDEA: permutations โ amount outer
// time = O(n * target), space = O(target)
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1; // Base: 1 way to make 0
// CRITICAL: Amount outer loop = PERMUTATIONS
for (int i = 1; i <= target; i++) { // โ Process each amount
for (int num : nums) { // โ Try every number for this amount
if (i >= num) {
dp[i] += dp[i - num];
}
}
}
return dp[target];
}
// Example: target=3, nums=[1,2]
// Result: 3 permutations
// {1,1,1}, {1,2}, {2,1} (Note: [1,2] and [2,1] are different)
๐ Detailed Trace Comparison: Why Loop Order Matters
Example: nums/coins = [1, 2], target/amount = 3
LC 518 (Combinations - Coin Outer):
Initialize: dp = [1, 0, 0, 0]
Process coin 1:
i=1: dp[1] += dp[0] = 1 โ [1, 1, 0, 0] // ways: {1}
i=2: dp[2] += dp[1] = 1 โ [1, 1, 1, 0] // ways: {1,1}
i=3: dp[3] += dp[2] = 1 โ [1, 1, 1, 1] // ways: {1,1,1}
Process coin 2:
i=2: dp[2] += dp[0] = 1+1=2 โ [1, 1, 2, 1] // ways: {1,1}, {2}
i=3: dp[3] += dp[1] = 1+1=2 โ [1, 1, 2, 2] // ways: {1,1,1}, {1,2}
// Note: Can't get {2,1} because
// all coin-1 uses are done before coin-2
Final: dp[3] = 2 โ
Only {1,1,1} and {1,2}
LC 377 (Permutations - Amount Outer):
Initialize: dp = [1, 0, 0, 0]
i=1 (building sum 1):
Try 1: dp[1] += dp[0] = 1 โ [1, 1, 0, 0] // ways: {1}
Try 2: skip (2 > 1)
i=2 (building sum 2):
Try 1: dp[2] += dp[1] = 1 โ [1, 1, 1, 0] // {1} + 1 = {1,1}
Try 2: dp[2] += dp[0] = 1+1=2 โ [1, 1, 2, 0] // {} + 2 = {2}
i=3 (building sum 3):
Try 1: dp[3] += dp[2] = 2 โ [1, 1, 2, 2] // {1,1} + 1 = {1,1,1}
// {2} + 1 = {2,1} โ
Try 2: dp[3] += dp[1] = 2+1=3 โ [1, 1, 2, 3] // {1} + 2 = {1,2} โ
Final: dp[3] = 3 โ
All three: {1,1,1}, {1,2}, {2,1}
Key Insight:
- LC 518 (Coin Outer): Once you finish processing coin-1, you never revisit it. This forces a canonical order (all 1s before all 2s), preventing duplicates like {1,2} and {2,1}.
- LC 377 (Amount Outer): For each sum, you ask โwhat was the last number added?โ Every number can be โlastโ, allowing both {1,2} and {2,1}.
When to Use Which
Use Combinations (Coin โ Amount) when:
- Problem asks for โnumber of waysโ without considering order
- [1,2,5] and [2,1,5] should be counted once
- Keywords: โcombinationsโ, โunique setsโ
Use Permutations (Amount โ Coin) when:
- Problem asks for different sequences/orderings
- [1,2] and [2,1] should be counted separately
- Keywords: โpermutationsโ, โdifferent orderingsโ, โsequencesโ
Complete Java Example: LC 518 Coin Change II
// java
// LC 518 - Coin Change II
// IDEA: count the ways to form each amount; coins outer keeps `{1,2}` and `{2,1}` as one
// time = O(n * amount), space = O(amount)
public int change(int amount, int[] coins) {
// dp[i] = total number of combinations that make up amount i
int[] dp = new int[amount + 1];
// Base case: There is exactly 1 way to make 0 amount (empty set)
dp[0] = 1;
// CRITICAL: Coin outer loop = COMBINATIONS
for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] += dp[i - coin];
}
}
return dp[amount];
}
Test Cases:
Input: amount = 5, coins = [1,2,5]
Output: 4
Combinations: {5}, {2,2,1}, {2,1,1,1}, {1,1,1,1,1}
Input: amount = 3, coins = [2]
Output: 0
Explanation: Cannot make 3 with only coins of 2
๐ Problem References
| Problem | LC # | Loop Order | What it Counts | File Reference |
|---|---|---|---|---|
| Coin Change II | 518 | Coin โ Amount | Combinations (order doesnโt matter) | leetcode_java/.../CoinChange2.java |
| Combination Sum IV | 377 | Amount โ Coin | Permutations (order matters) | leetcode_java/.../CombinationSumIV.java |
๐ก Memory Trick:
- โCoin firstโ = Combinations (both start with โCโ)
- โAmount firstโ = Arrangements/Permutations (both start with โAโ)
๐ Final Summary: Complete Pattern Comparison
| Aspect | LC 518: Coin Change II (Combinations) |
LC 377: Combination Sum IV (Permutations) |
|---|---|---|
| What it counts | Unique sets (order doesnโt matter) | Different sequences (order matters) |
| Example | [1,2] = [2,1] (same) | [1,2] โ [2,1] (different) |
| Outer Loop | for (int coin : coins) |
for (int i = 1; i <= target; i++) |
| Inner Loop | for (int i = coin; i <= amount; i++) |
for (int num : nums) |
| DP Transition | dp[i] += dp[i - coin] |
dp[i] += dp[i - num] |
| Base Case | dp[0] = 1 |
dp[0] = 1 |
| Result for nums=[1,2], target=3 |
2 combinations: {1,1,1}, {1,2} |
3 permutations: {1,1,1}, {1,2}, {2,1} |
| Why it works | Processing coin-1 completely before coin-2 forces canonical order โ no {2,1} | For each sum, try every number as โlastโ โ allows all orderings |
| File Reference | CoinChange2.java |
CombinationSumIV.java |
๐ฅ The ONLY Difference:
// java
// IDEA: the two nestings printed together โ the only difference is which loop is outer
// time = O(n * amount), space = O(amount)
// LC 518: Combinations
for (int coin : coins) // โ ITEM OUTER
for (int i = coin; i <= amount; i++)
// LC 377: Permutations
for (int i = 1; i <= target; i++) // โ TARGET OUTER
for (int num : nums)
Both use the EXACT SAME transition: dp[i] += dp[i - item]
Why the Guard Is if (i - coin >= 0), Not if (i == coin)
๐ The Question: Why use if (i >= coin) instead of if (i == coin)?
This is a fundamental concept in understanding how Dynamic Programming builds on previously solved subproblems.
The Short Answer
i == coinonly checks if a single coin matches the amounti >= coinchecks if a coin can be combined with a previous sum to reach the amount
The Logic of i - coin >= 0
When we calculate dp[i], we arenโt just looking for one coin that equals i. We are looking for a coin coin that, when subtracted from i, leaves a remainder that we already know how to solve.
i: The total amount we are trying to reach right nowcoin: The value of the coin we just picked upi - coin: The โremainderโ or the amount left over
If i - coin >= 0, the coin fits and the remainder is a subproblem we have already calculated, because we fill the table from 0 up to amount. The == 0 case is not special-cased: it reads dp[0], which the base case already set. That is exactly why the guard is >= and not >.
The DP looks back at dp[i - coin] to reuse that solution!
A Concrete Example
Imagine coins = [2] and we want to find dp[4] (how to make 4 cents).
- We try the coin
coin = 2 i - coinis4 - 2 = 2- Since
2 > 0, we donโt stop. We look atdp[2] - We already calculated
dp[2] = 1(it took one 2-cent coin to make 2 cents) - So,
dp[4] = dp[2] + 1 = 2
If we only used if (i - coin == 0):
- We would only ever find that
dp[2] = 1 - When we got to
dp[4], the condition4 - 2 == 0would be false - We would incorrectly conclude that we canโt make 4 cents!
The Three Scenarios
When checking i - coin:
Result of i - coin |
Meaning | Action |
|---|---|---|
Negative (< 0) |
The coin is too big for this amount | Skip this coin |
Zero (== 0) |
This single coin matches the amount perfectly | dp[i] = 1 |
Positive (> 0) |
This coin fits, and we need to check the โremainderโ | dp[i] = dp[remainder] + 1 |
The last two rows are the same line of code โ dp[i] = dp[i - coin] + 1 โ because dp[0] is
already seeded to 0. That is why one guard, i - coin >= 0, covers both.
๐ก Key Insight
The condition if (i >= coin) covers both the case where a coin matches exactly and the case where a coin is just one piece of a larger puzzle.
Complete Example with Trace
Input: coins = [1,2,5], amount = 11
Setup:
- DP Array:
int[12](Indices 0 to 11) - Initialization:
dp[0] = 0, all others =12(our โInfinityโ)
Step-by-Step Trace:
Amounts 1 through 4:
- At
i=1: Only coin1fits (1 >= 1).dp[1] = dp[0] + 1 = 1 - At
i=2:- Coin
1:dp[2] = dp[1] + 1 = 2 - Coin
2:dp[2] = dp[0] + 1 = 1(Winner: Min is 1)
- Coin
- At
i=3:- Coin
1:dp[3] = dp[2] + 1 = 2 - Coin
2:dp[3] = dp[1] + 1 = 2 dp[3] = 2(e.g.,2+1or1+1+1)
- Coin
- At
i=4:- Coin
1:dp[4] = dp[3] + 1 = 3 - Coin
2:dp[4] = dp[2] + 1 = 2 dp[4] = 2(e.g.,2+2)
- Coin
Amount 5 (The first big jump):
- Coin
1:dp[5] = dp[4] + 1 = 3 - Coin
2:dp[5] = dp[3] + 1 = 3 - Coin
5:dp[5] = dp[0] + 1 = 1 - Result:
dp[5] = 1(Matches perfectly)
Amount 10:
- Coin
1:dp[10] = dp[9] + 1 = 4 - Coin
2:dp[10] = dp[8] + 1 = 4 - Coin
5:dp[10] = dp[5] + 1 = 2 - Result:
dp[10] = 2(this represents5+5)
The Final Goal: Amount 11:
-
Try Coin
1:- Remainder:
11 - 1 = 10 - Look up
dp[10]: It is2 - Calculation:
dp[11] = dp[10] + 1 = 3
- Remainder:
-
Try Coin
2:- Remainder:
11 - 2 = 9 - Look up
dp[9]: It is3(e.g.,5+2+2) - Calculation:
dp[11] = dp[9] + 1 = 4
- Remainder:
-
Try Coin
5:- Remainder:
11 - 5 = 6 - Look up
dp[6]: It is2(e.g.,5+1) - Calculation:
dp[11] = dp[6] + 1 = 3
- Remainder:
Final Comparison: dp[11] = min(3, 4, 3) = 3
Why the remainder i - coin > 0 worked
When calculating for 11, the algorithm didnโt have to โre-solveโ how to make 10 or 6. It just looked at the table:
- โOh, I know the best way to make 10 is 2 coins (
5+5)โ - โIf I add my 1 coin to that, I get 11 using 3 coins (
5+5+1)โ
Summary Table (Simplified)
| i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| dp[i] | 0 | 1 | 1 | 2 | 2 | 1 | 2 | 2 | 3 | 3 | 2 | 3 |
The DP Code Pattern
// java
// LC 322 - Coin Change
// IDEA: min coins per amount; order is irrelevant so either nesting works
// time = O(n * amount), space = O(amount)
public int coinChange(int[] coins, int amount) {
if (amount == 0) return 0;
// dp[i] = min coins to make amount i
int[] dp = new int[amount + 1];
// Initialize with "Infinity" (amount + 1 is safe)
Arrays.fill(dp, amount + 1);
// Base case: 0 coins needed for 0 amount
dp[0] = 0;
// Iterate through every amount from 1 to amount
for (int i = 1; i <= amount; i++) {
// For each amount, try every coin
for (int coin : coins) {
// CRITICAL CONDITION: Check if coin fits
if (i >= coin) {
// DP equation: Min of (current value) OR
// (1 coin + coins needed for remainder)
dp[i] = Math.min(dp[i], dp[i - coin] + 1);
}
}
}
// If value is still "Infinity", we couldn't reach it
return dp[amount] > amount ? -1 : dp[amount];
}
Reference: See leetcode_java/src/main/java/LeetCodeJava/DynamicProgramming/CoinChange.java:356-408 for detailed implementation.
Deep Dive: Difference-Keyed Knapsack โ LC 956 ๐๏ธ Priority 4 of 5 โ High value โ a gap here costs you rounds
When to Use a Difference Key
Every problem above indexes the table by a total โ dp[capacity], dp[amount], dp[sum].
Some problems instead split the items into two groups and only care how far apart the groups
are. Then the total is irrelevant and the right key is the difference:
dp[capacity] = best value -> "how much have I packed?"
dp[difference] = best height -> "how unbalanced are the two piles?"
Recognise it by the shape of the answer:
- โsplit into two subsets with equal sum, maximise that sumโ (LC 956 โ the two steel supports)
- โminimise
|sum(A) - sum(B)|โ (LC 1049 Last Stone Weight II, LC 2035 Partition Array Into Two Arrays) - โmake two piles equal, items may be left outโ
LC 416 / LC 1049 are the same family solved with a total. With small sums you can key on
sumand readdp[total/2]at the end. LC 956 cannot: it wants the height of the balanced pair, and rods can be discarded โ so the value you carry and the value you key by are different things. That is exactly what the difference key buys.
The State
dp[d] = the tallest achievable TALLER side, over all ways to split some subset
of the rods so far into two piles differing by exactly d
dp[0] = 0 (two empty piles, difference 0, height 0)
answer = dp[0] after all rods -- difference 0 means equal, and we kept the max height
Each rod has three choices, not two, and this is where it differs from 0/1 knapsack:
skip it -> (d, taller) unchanged
put on TALLER -> (d + x, taller + x) the gap widens
put on SHORTER -> (|d - x|, max(taller, shorter+x)) the gap closes, and may flip sides
where shorter = taller - d
The |d - x| and the max(...) together handle the case where the short pile overtakes the tall
one โ the two piles swap roles and the difference reflects back off zero.
// java
// LC 956 - Tallest Billboard
// IDEA: key the table by the DIFFERENCE between the two supports, store the taller
// support's height. Answer = dp[0], the best height at difference zero.
// time = O(n * sum), space = O(sum)
public int tallestBillboard(int[] rods) {
int sum = 0;
for (int r : rods) sum += r;
int[] dp = new int[sum + 1];
Arrays.fill(dp, -1); // NOTE !!! -1 = unreachable; 0 is a real, reachable height
dp[0] = 0;
for (int x : rods) {
int[] prev = dp.clone(); // "skip x" is prev carried forward, already in dp
for (int d = 0; d <= sum; d++) {
if (prev[d] < 0) continue;
int taller = prev[d], shorter = taller - d;
// 1) x joins the taller pile -> gap widens by x
if (d + x <= sum) dp[d + x] = Math.max(dp[d + x], taller + x);
// 2) x joins the shorter pile -> gap becomes |d - x|, piles may swap
int nd = Math.abs(d - x);
dp[nd] = Math.max(dp[nd], Math.max(taller, shorter + x));
}
}
return dp[0];
}
# python
# LC 956 - Tallest Billboard
# IDEA: dict from difference -> tallest "taller side"; only reachable differences are stored
# time = O(n * sum), space = O(sum)
def tallestBillboard(rods):
dp = {0: 0} # difference -> height of the taller support
for x in rods:
prev = dict(dp) # snapshot: each rod is used at most once
for d, taller in prev.items():
shorter = taller - d
nd, nt = d + x, taller + x # x on the taller side
dp[nd] = max(dp.get(nd, 0), nt)
nd2 = abs(d - x) # x on the shorter side
dp[nd2] = max(dp.get(nd2, 0), max(taller, shorter + x))
return dp[0]
Trace โ rods = [1, 2, 3, 6]
start {0: 0}
after 1 {0: 0, 1: 1}
after 2 {0: 0, 1: 2, 2: 2, 3: 3}
after 3 {0: 3, 1: 3, 2: 4, 3: 3, 4: 5, 5: 5, 6: 6}
after 6 {0: 6, 1: 6, 2: 7, 3: 6, ...} -> dp[0] = 6, supports {6} and {1,2,3}
dp[0] jumps from 0 to 3 when the third rod balances {3} against {1,2}, and to 6 when the
6 balances the whole rest.
Common Pitfalls โ difference key โ ๏ธ
- Initialising the array to
0instead of-1. Height0at differencedis a legal state only ford = 0; a zero-filled array claims every difference is reachable for free and the answer comes out too large. - Mutating
dpwhile iterating it (Python) or forgetting theclone()(Java). Without the snapshot a rod can be placed on both piles in one pass. - Storing the shorter side instead of the taller. Either convention works, but the transition
formulas are different โ pick one and derive
shorter = taller - dfrom it consistently. d + xoverflowing the array. Cap the loop atsum; a difference larger than the total is meaningless.
Similar LeetCode Problems โ two-pile splits ๐
| Problem | Key |
|---|---|
| LC 1049 Last Stone Weight II | minimise the difference โ the same table, read the smallest reachable d |
| LC 416 Partition Equal Subset Sum | feasibility only โ a boolean dp[sum] is enough |
| LC 2035 Partition Array Into Two Arrays | n <= 30, so meet-in-the-middle beats a difference table |
| LC 494 Target Sum | signs instead of piles โ algebra turns it back into a subset-sum on the total |
Deep Dive: Group 0/1 Knapsack โ LC 4040 ๐ Priority 4 of 5 โ High value โ a gap here costs you rounds
When an Item Becomes a Group
In plain 0/1 knapsack an item is a single (weight, value) pair and the decision is
take / skip. In a group knapsack each item arrives as a menu of mutually exclusive
options, and the decision is which one, or none:
0/1 item i -> (w, v) take it or don't
group item i -> {(w, c), (w', c'), ...} take AT MOST ONE of these
Recognise it whenever an item can be used in one of several forms:
| Problem | The group isโฆ | Per-group rule |
|---|---|---|
| LC 4040 Minimum Operations to Form Subset Sum I | one x and every value it can be transformed into, with the op count as the cost |
โค 1 |
| LC 1155 Number of Dice Rolls With Target Sum | one die and its faces 1..f |
exactly 1 |
| LC 2218 Maximum Value of K Coins From Piles | one pile and each of its prefixes (0..k coins) |
โค 1 |
| LC 2585 Number of Ways to Earn Points | one question type used 0, 1, โฆ count times |
โค 1 |
| LC 474 Ones and Zeroes | โ (counter-example) | plain 0/1 with a 2-D capacity, no menu |
Bounded knapsack is a group knapsack. โUsable up to
ktimesโ is the group{(w, v), (2w, 2v), โฆ, (kw, kv)}; the binary split in the table at the top of this file is just the faster way to spell the same thing.
The State โ same axis, new inner choice
The capacity axis does not change. Only what happens inside it does:
0/1 dp[s] = best( dp[s], dp[s - w] + v ) ONE candidate
group dp[s] = best( dp[s], best over the item's options ) |group| candidates
LC 4040 asks for a minimum cost, so the table is seeded with INF rather than 0/False:
dp[s] = fewest operations to make SOME subset of the elements seen so far sum to exactly s
dp[0] = 0, everything else INF
answer = dp[sum], or -1 if it never left INF
The One Rule: at most one option per group โ ๏ธ
Backward iteration is what stops an item being reused in plain 0/1 โ but it does not stop two different options of the same group from both being taken. Two loop orders enforce the group rule; a third, the one that looks most like the 0/1 template, is wrong:
โ
snapshot read prev, write into a copy every option competes against the state BEFORE the item
โ
capacity OUTER for s in W..0: dp[s-w] is a smaller index, not yet touched by this item
for (w, c) in group:
โ options OUTER for (w, c) in group: option B reads a dp[] that option A has already updated
for s in W..w:
The failure is not subtle. nums = [5], sum = 3: the group for 5 is {(2, 1), (1, 2)}
(5 -> 2, and 5 -> 2 -> 1). The answer is -1 โ one element cannot be two members of a subset โ
but the options-outer loop happily builds 2 + 1 = 3 and reports 3.
# python
# The group knapsack skeleton -- both correct orders, minimisation flavour
# time = O(n * W * |group|), space = O(W)
def group_knapsack(groups, W):
INF = float('inf')
dp = [INF] * (W + 1)
dp[0] = 0
for options in groups: # options = [(weight, cost), ...]
# form A -- snapshot: dp is read-only for the whole group
new_dp = dp[:]
for w, c in options:
for s in range(w, W + 1):
if dp[s - w] != INF:
new_dp[s] = min(new_dp[s], dp[s - w] + c)
dp = new_dp
# form B -- in place, capacity OUTER and backward (no copy needed)
# for s in range(W, -1, -1):
# for w, c in options:
# if s >= w and dp[s - w] != INF:
# dp[s] = min(dp[s], dp[s - w] + c)
return dp[W]
Building LC 4040โs Group: only the two pure chains
The modelling half of the problem is deciding what the menu contains. An element x may be
doubled and halved, but all its multiplications must come before all its divisions โ and that
rule collapses the menu to two straight chains:
x, 2x, 4x, ... k doublings, cost k
x, x//2, x//4, ... k halvings, cost k
A mixed run is never worth it: k doublings then j halvings lands on x * 2^(k-j) โ doubling
loses no low bits, so the halvings undo it exactly โ which a pure chain already reaches at cost
|k - j| instead of k + j. Only divide-then-multiply could reach something genuinely new
(5 -> 2 -> 4, which no pure chain gives), and that is precisely what the problem forbids.
x = 10, sum = 13 -> (10, 0), (5, 1), (2, 2), (1, 3) 20 is already over sum
x = 2, sum = 13 -> (2, 0), (4, 1), (8, 2), (1, 1)
Cut both chains as soon as they stop being useful โ the doubling chain past sum (it only grows),
the halving chain at 0 (it can never help a positive sum).
# python
# LC 4040 - Minimum Operations to Form Subset Sum I
# IDEA: group 0/1 knapsack -- each x is a menu of (value, ops); take at most one entry per x
# time = O(n * sum * log(max(x, sum))), space = O(sum)
def minOperations(nums, sum):
INF = float('inf')
dp = [INF] * (sum + 1) # dp[s] = min ops for a subset summing to s
dp[0] = 0
for x in nums:
options = []
if x <= sum:
options.append((x, 0)) # NOTE !!! keep x untouched -- the zero-cost option
value, op = x, 0
while value <= sum: # x, 2x, 4x, ...
if op > 0:
options.append((value, op))
value *= 2
op += 1
value, op = x, 0
while value > 0: # x//2, x//4, ...
value, op = value // 2, op + 1
if value == 0:
break
if value <= sum:
options.append((value, op))
new_dp = dp[:] # NOTE !!! snapshot => x is used at most once,
for value, cost in options: # in at most one of its forms
for s in range(value, sum + 1):
if dp[s - value] != INF:
new_dp[s] = min(new_dp[s], dp[s - value] + cost)
dp = new_dp
return -1 if dp[sum] == INF else dp[sum]
// java
// LC 4040 - Minimum Operations to Form Subset Sum I
// IDEA: same group knapsack, written in place with the capacity loop OUTER and backward
// time = O(n * sum * log(max(x, sum))), space = O(sum)
public int minOperations(int[] nums, int sum) {
final int INF = Integer.MAX_VALUE / 2;
int[] dp = new int[sum + 1];
Arrays.fill(dp, INF);
dp[0] = 0;
for (int x : nums) {
List<int[]> options = new ArrayList<>();
for (long v = x, op = 0; v <= sum; v *= 2, op++) // x, 2x, 4x, ... (op = 0 keeps x)
options.add(new int[]{(int) v, (int) op});
for (long v = x, op = 0; v > 0; ) { // x/2, x/4, ...
v /= 2;
op++;
if (v == 0) break;
if (v <= sum) options.add(new int[]{(int) v, (int) op});
}
for (int s = sum; s >= 0; s--) { // NOTE !!! capacity OUTER, backward
for (int[] o : options) { // options INNER
int value = o[0], cost = o[1];
if (s >= value && dp[s - value] != INF)
dp[s] = Math.min(dp[s], dp[s - value] + cost);
}
}
}
return dp[sum] >= INF ? -1 : dp[sum];
}
Trace โ nums = [10, 2], sum = 13
groups 10 -> (10,0) (5,1) (2,2) (1,3)
2 -> (2,0) (4,1) (8,2) (1,1)
start {0:0}
after 10 {0:0, 1:3, 2:2, 5:1, 10:0}
after 2 {0:0, 1:1, 2:0, 3:3, 4:1, 5:1, 6:2, 7:1, 8:2, 9:2, 10:0, 11:1, 12:0, 13:3}
^
dp[13] = dp[5] + 2 = 1 + 2 -> 10->5 and 2->4->8
Note dp[1] improving from 3 to 1: the first group reached 1 only as 10 -> 5 -> 2 -> 1,
the second gets there with 2 -> 1. Each row is the cheapest way to that sum, not a set of them.
Common Pitfalls โ group knapsack โ ๏ธ
- Options outer with an in-place
dp. The 0/1 muscle memory, and it silently mixes two forms of one element โ see thenums = [5], sum = 3case above. - Dropping the zero-cost option. Without
(x, 0)the element can only be used transformed, sonums = [4], sum = 4returns-1. - Enumerating mixed chains. Wasted work here โ but the pruning argument is a property of this problemโs ordering rule. If divide-then-multiply were allowed the two pure chains would be incomplete, so re-read the rule before reusing the shortcut.
- Not bounding the chains. The doubling chain never returns once it passes
sum, and a halved value of0cannot contribute to a positive sum; both are infinite/dead loops otherwise. - Reading
dp[sum]without theINFcheck, which returns a huge sentinel instead of-1. In Java also keepINFatMAX_VALUE / 2sodp[...] + costcannot overflow.
Similar LeetCode Problems โ groups & menus ๐
| Problem | Key |
|---|---|
| LC 1155 Number of Dice Rolls With Target Sum | exactly one face per die โ so no โskipโ, and dp is a count: dp[i][t] += dp[i-1][t-f] |
| LC 2218 Maximum Value of K Coins From Piles | group = prefixes of a pile; prefix-sum each pile first, then it is this template with max |
| LC 2585 Number of Ways to Earn Points | group = โuse this type j timesโ, j = 0..count โ bounded knapsack read as a group |
| LC 1449 Form Largest Integer With Digits That Add up to Target | one unbounded menu shared by all positions โ a group knapsack it is not; compare the loop order |
| LC 474 Ones and Zeroes | plain 0/1, but the capacity is a (zeros, ones) pair โ the other way an itemโs axis can grow |
| LC 4040 Minimum Operations to Form Subset Sum I | the menu is derived, not given โ most of the work is proving which options are reachable and cheapest |
Pattern Selection Strategy
Does one item offer SEVERAL mutually exclusive options?
โ
โโ YES โโโบ Group Knapsack [4040, 1155, 2218, 2585]
โ for group in groups:
โ for w in range(W, -1, -1): # CAPACITY outer, backward
โ for (weight, cost) in group: # options inner
โ โโ or snapshot dp and let every option read the pre-group state
โ โโ options-outer + in-place dp is the classic bug: two forms of one item
โ
โโ NO โโโบ Is each item reusable?
โ
โโ NO โโโบ 0/1 Knapsack
โ for item in items:
โ for w in range(W, weight-1, -1): # BACKWARD
โ โโ asks "can we hit the sum?" -> boolean dp
โ โโ asks "how many ways?" -> dp[j] += dp[j-w]
โ โโ asks "best value?" -> dp[j] = max(dp[j], dp[j-w]+v)
โ
โโ YES โโโบ Does order matter?
โ
โโ NO (combinations, {1,2} == {2,1}) โโโบ items outer, amount inner [518]
โโ YES (permutations, {1,2} != {2,1}) โโโบ amount outer, items inner [377]
โโ Min/max only (order irrelevant) โโโบ either nesting [322, 279]
Summary
| If you remember one thing per row | โฆ it is this |
|---|---|
| 0/1 vs unbounded | the inner loop direction: backward blocks reuse, forward permits it |
| Combinations vs permutations | the loop nesting: items-outer counts sets, amount-outer counts sequences |
| Partition problems | โsplit into two equal halvesโ โ subset-sum to total / 2 |
| LC 494 Target Sum | sum1 = (total + T) / 2, but guard abs(T) <= total and (total + T) even first |
| The guard | i - coin >= 0, not > 0 โ the == 0 case reads the seeded dp[0] |
| Bounded knapsack | binary-split each item into 1, 2, 4, โฆ copies, then run plain 0/1 |
| Group knapsack | put the capacity loop outside the options loop โ options-outer takes two forms of one item |
| LC 4040 | the group is the two pure x2 / x//2 chains, cost = length; โmultiply before divideโ is what makes mixed runs redundant |