Knapsack DP

Dynamic Programming & RecursionPriority 5 of 5 โ€” Must know โ€” expect it in almost every loopMust know Updated Sep 18, 2026
Section priorityPriority 5 of 5 โ€” Must know โ€” expect it in almost every loopMust knowPriority 4 of 5 โ€” High value โ€” a gap here costs you roundsHigh valuePriority 3 of 5 โ€” Worth knowing โ€” usually a variant of a must-know patternWorth knowingPriority 2 of 5 โ€” Niche โ€” read once, revisit only if a company is known to askNicheMarked on the sections that carry it โ€” unmarked sections are background/reference.

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 โ€” n items, W capacity/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

text
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                     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:

text
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 โ†’ TARGET

for 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 โ†’ ITEM

for 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
// 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:

  1. Same DP Transition (dp[i] += dp[i - item]) for:

    • Combinations (Item โ†’ Target)
    • Permutations (Target โ†’ Item)
    • Only difference: Loop order!
  2. 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)
  3. Direction Matters for knapsack:

    • Backwards โ†’ prevents reuse (0/1)
    • Forwards โ†’ allows reuse (unbounded)

๐ŸŽฏ Pattern Selection Decision Tree

text
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.

text
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
// 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
// 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)

text
โŒ 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

text
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):

text
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):

text
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
// 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 โš ๏ธ

  1. Iterating forwards instead of backwards

    • Will allow reusing same item multiple times
    • Use backwards iteration for 0/1 knapsack
  2. 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!
  3. Not recognizing the โ€œpartitionโ€ pattern

    • โ€œDifference between groupsโ€ โ†’ Think partition
    • โ€œSplit into two teamsโ€ โ†’ Think partition
    • โ€œDivide arrayโ€ โ†’ Think partition
  4. 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
// 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 coin 1
  • Impossible to place a 1 after a 2, forcing non-decreasing order
  • Result: Only combinations (order doesnโ€™t matter)

Example Trace: coins = [1,2], amount = 3

text
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
// 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

text
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
// 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
// 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):

text
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):

text
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
// 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:

text
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
// 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 == coin only checks if a single coin matches the amount
  • i >= coin checks 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 now
  • coin: The value of the coin we just picked up
  • i - 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).

  1. We try the coin coin = 2
  2. i - coin is 4 - 2 = 2
  3. Since 2 > 0, we donโ€™t stop. We look at dp[2]
  4. We already calculated dp[2] = 1 (it took one 2-cent coin to make 2 cents)
  5. 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 condition 4 - 2 == 0 would 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 coin 1 fits (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)
  • 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+1 or 1+1+1)
  • 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)

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 represents 5+5)

The Final Goal: Amount 11:

  1. Try Coin 1:

    • Remainder: 11 - 1 = 10
    • Look up dp[10]: It is 2
    • Calculation: dp[11] = dp[10] + 1 = 3
  2. Try Coin 2:

    • Remainder: 11 - 2 = 9
    • Look up dp[9]: It is 3 (e.g., 5+2+2)
    • Calculation: dp[11] = dp[9] + 1 = 4
  3. Try Coin 5:

    • Remainder: 11 - 5 = 6
    • Look up dp[6]: It is 2 (e.g., 5+1)
    • Calculation: dp[11] = dp[6] + 1 = 3

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
// 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:

text
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 sum and read dp[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

text
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:

text
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
// 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
# 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]

text
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 0 instead of -1. Height 0 at difference d is a legal state only for d = 0; a zero-filled array claims every difference is reachable for free and the answer comes out too large.
  • Mutating dp while iterating it (Python) or forgetting the clone() (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 - d from it consistently.
  • d + x overflowing the array. Cap the loop at sum; 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:

text
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 k timesโ€ 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:

text
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:

text
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:

text
โœ… 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
# 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:

text
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.

text
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
# 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
// 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

text
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 the nums = [5], sum = 3 case above.
  • Dropping the zero-cost option. Without (x, 0) the element can only be used transformed, so nums = [4], sum = 4 returns -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 of 0 cannot contribute to a positive sum; both are infinite/dead loops otherwise.
  • Reading dp[sum] without the INF check, which returns a huge sentinel instead of -1. In Java also keep INF at MAX_VALUE / 2 so dp[...] + cost cannot 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

text
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