Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Dynamic Programming

Builds on Linear DP, which you haven’t finished yet.

P1MediumMental model~5 min speedrun

Unbounded Knapsack (Coin Change, Coin Change II)

The problem

Coin Change (LC 322)(opens leetcode.com)

You're given an array coins of coin denominations, each reusable as many times as you like, and a target amount of money.

Work out the smallest number of coins whose values add up to exactly amount, or return -1 if no combination of the given coins reaches that amount exactly.

Example 1
Input:coins = [1,2,5], amount = 11
Output:3
11 = 5 + 5 + 1

Unbounded Knapsack

items & capacities ➔ capacity loop runs upward so an item can repeat ➔ dp[w]
Spot it when: You build a target amount from items that can each be used any number of times, and you want the fewest items or the number of ways: coins, perfect squares, steps of fixed sizes.
Unbounded Knapsack
Python
1def coin_change(coins: list[int], amount: int) -> int:
2 """Unbounded Knapsack: the fewest coins that make amount, each coin usable any number of times (forward loop).
3 Time: O(N * amount) | Space: O(amount)
4 """
5 # dp: dp[w] is the fewest coins that make amount w; infinity marks w as not yet reachable
6 dp = [float('inf')] * (amount + 1)
7 dp[0] = 0
8 for coin in coins:
9 for w in range(coin, amount + 1):
10 # 1. Reuse This Coin Forward:
11 # The forward loop lets dp[w - coin] already include this coin, so one coin may be used any number of times.
12 dp[w] = min(dp[w], dp[w - coin] + 1)
13 return int(dp[amount]) if dp[amount] != float('inf') else -1
Invariant: After each coin, dp[w] is the fewest coins that make w using only the coins processed so far, each any number of times.

Treat each amount as its own subproblem: dp[w] is the fewest coins that make w, and each coin updates it from dp[w - coin]. Loop w upward, so dp[w - coin] may already include this coin and the coin can repeat. Start from infinity, which marks an amount as not yet reachable, and only dp[0] = 0. The trap is the loop direction: for w in range(coin, amount + 1), not downward, which would use each coin at most once.

See it run

Coin Change, step by step

coin - · w - · dp[amount] inf

coin 1 · w - · dp[amount] inf

coin 1 · w 1 · dp[amount] inf

coin 1 · w 2 · dp[amount] inf

coin 1 · w 3-10 · dp[amount] inf

coin 1 · w 11 · dp[amount] 11

coin 2 · w - · dp[amount] 11

coin 2 · w 2 · dp[amount] 11

coin 2 · w 3 · dp[amount] 11

coin 2 · w 4 · dp[amount] 11

coin 2 · w 5-10 · dp[amount] 11

coin 2 · w 11 · dp[amount] 6

coin 5 · w - · dp[amount] 6

coin 5 · w 5 · dp[amount] 6

coin 5 · w 6 · dp[amount] 6

coin 5 · w 7-9 · dp[amount] 6

coin 5 · w 10 · dp[amount] 6

coin 5 · w 11 · dp[amount] 3

coin - · w - · dp[amount] 3

Row k is the list dp after the k-th coin. Column w is the amount w.

  • write the cell being written
  • read the cell dp[w - coin], which may already include this coin
  • answer the final answer

Start: coins = [1, 2, 5] and amount = 11. dp[0] = 0 because the amount 0 needs no coin. Every other cell is inf, which means the amount cannot be made yet.

Coin 1. The loop runs w upward from 1 to 11. So dp[w - 1] may already include this coin.

w = 1: dp[1] = min(inf, dp[0] + 1) = min(inf, 0 + 1) = 1.

w = 2: dp[2] = min(inf, dp[1] + 1) = min(inf, 1 + 1) = 2. dp[1] was written earlier in this pass, so coin 1 is used again.

w = 3 to 10: the same rule runs on each sum in order. New values: 3, 4, 5, 6, 7, 8, 9, 10.

w = 11: dp[11] = min(inf, dp[10] + 1) = min(inf, 10 + 1) = 11. dp[10] was written earlier in this pass, so coin 1 is used again.

Coin 2. The loop runs w upward from 2 to 11. So dp[w - 2] may already include this coin.

w = 2: dp[2] = min(2, dp[0] + 1) = min(2, 0 + 1) = 1.

w = 3: dp[3] = min(3, dp[1] + 1) = min(3, 1 + 1) = 2.

w = 4: dp[4] = min(4, dp[2] + 1) = min(4, 1 + 1) = 2. dp[2] was written earlier in this pass, so coin 2 is used again.

w = 5 to 10: the same rule runs on each sum in order. New values: 3, 3, 4, 4, 5, 5.

w = 11: dp[11] = min(11, dp[9] + 1) = min(11, 5 + 1) = 6. dp[9] was written earlier in this pass, so coin 2 is used again.

Coin 5. The loop runs w upward from 5 to 11. So dp[w - 5] may already include this coin.

w = 5: dp[5] = min(3, dp[0] + 1) = min(3, 0 + 1) = 1.

w = 6: dp[6] = min(3, dp[1] + 1) = min(3, 1 + 1) = 2.

w = 7 to 9: the same rule runs on each sum in order. New values: 2, 3, 3.

w = 10: dp[10] = min(5, dp[5] + 1) = min(5, 1 + 1) = 2. dp[5] was written earlier in this pass, so coin 5 is used again.

w = 11: dp[11] = min(6, dp[6] + 1) = min(6, 2 + 1) = 3. dp[6] was written earlier in this pass, so coin 5 is used again.

All coins are processed. dp[11] = 3, the fewest coins that make 11.

start
00write
1inf
2inf
3inf
4inf
5inf
6inf
7inf
8inf
9inf
10inf
11inf
coin 1
coin 2
coin 5
1 / 19
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Looping w downward when items can be reused

    Mistake
    for w in range(amount, coin - 1, -1):
    Fix
    for w in range(coin, amount + 1):

    What happens: Going upward, dp[w - coin] may already include this coin, so dp[w] can use it again. That is what 'any number of times' needs. Going downward reads dp[w - coin] before this pass updates it, so each coin is used at most once, which is the 0/1 rule. With coins = [2], amount 4 needs the coin twice.

    Example: coins = [2], amount = 4
    Mistake: returns -1Fix: returns 2
  2. 2

    Gotcha 2: Putting the amounts in the outer loop to count combinations

    Mistake
    for w in range(1, amount + 1):
        for coin in coins:
    Fix
    for coin in coins:
        for w in range(coin, amount + 1):

    What happens: Coin Change II (LC 518) counts combinations, so 1 + 2 and 2 + 1 are one way. With coins in the outer loop, each coin is added after the coins before it, in one order only. With amounts outside, every order is counted as its own way. A counting problem starts from dp[0] = 1, the one way to make 0.

    Example: Coin Change II (LC 518): amount = 3, coins = [1, 2]
    Mistake: returns 3Fix: returns 2
  3. 3

    Gotcha 3: Filling dp with 0 instead of infinity

    Mistake
    dp = [0] * (amount + 1)
    Fix
    dp = [float('inf')] * (amount + 1)

    What happens: dp[w] is the fewest coins for amount w, and infinity means w cannot be made. dp[0] = 0 is the only real start. With 0 everywhere, min keeps 0 for every amount. So an amount that cannot be made looks free.

    Example: coins = [2], amount = 3
    Mistake: returns 0Fix: returns -1
Not this one if…
  • Each item can be used at most once, as in Partition Equal Subset Sum (LC 416): the capacity loop runs downward, so an item cannot fill dp[w - item] and then dp[w] in the same pass. See 0/1 Knapsack
  • You must return every combination itself, not just the best value or the number of ways, as in Combination Sum (LC 39): a table of numbers per amount can't list them. See Combinations

Deep Dive

The state is an amount, and each item may repeat

In an unbounded knapsack problem, each item can be used any number of times. In Coin Change (LC 322), the state is an amount w, and dp[w] is the fewest coins that make w using only the coins processed so far.

The answer type changes with the problem. In coin_change, dp[w] is a minimum. In Coin Change II (LC 518), it is a number of ways. In Perfect Squares (LC 279), the items are the squares 1, 4, 9 and so on, and dp[w] is the fewest squares that add up to w.

After coin k, dp describes the first k coins, each used any number of times. The next coin adds the choice: end with this coin, so dp[w] can become dp[w - coin] + 1.

Example: for coins = [1, 2, 5] and amount 11, the answer is 3, namely 5 + 5 + 1. After the coin 1, dp is [0, 1, 2, ..., 11]. After the coin 2, dp[11] is 6. After the coin 5, dp[11] is 3.

Why the forward loop lets a coin repeat

The inner loop runs w upward from coin. When the loop updates dp[w], the smaller index w - coin was already updated in this pass. So dp[w - coin] may already include this coin, and dp[w] can use it again.

This is exactly what 'any number of times' needs. For coins = [2] and amount 4, dp[2] becomes 1, and then dp[4] reads dp[2] and becomes 2. The answer is 2.

A downward loop would read dp[w - coin] before it is updated, so each coin would be used at most once. That is the rule of 0/1 Knapsack. With a downward loop, coin_change([2], 4) returns -1 instead of 2.

Infinity or 0 for the unreachable start

In a minimum problem, an amount that cannot be made yet must lose every comparison. So dp starts as infinity, and only dp[0] = 0 is real. Infinity marks an amount as not reachable yet.

With 0 everywhere, min keeps 0 for every amount, and an amount that cannot be made looks free. coin_change([2], 3) would return 0 instead of -1.

At the end, the code returns -1 when dp[amount] is still infinity. A counting problem uses a different start: dp[0] = 1, the one way to make 0 with no coin, and 0 for the others.

Which loop is outer when you count

For a minimum, the order of the two loops does not change the result. For a count, it does.

With coins in the outer loop, as in coin_change, each combination is built in the order of the coin list, so it is counted once. This is Coin Change II (LC 518): for amount 3 and coins [1, 2], the answer is 2, namely 1 + 1 + 1 and 1 + 2.

With amounts in the outer loop, 1 + 2 and 2 + 1 are different sequences, and both are counted. This is Combination Sum IV (LC 377), which counts ordered sequences. For nums = [1, 2, 3] and target 4 the answer is 7. The same loop on Coin Change II (LC 518) gives 3 for amount 3, which is wrong there.

The cost is O(N * amount) time and O(amount) space, where N is the number of coins. Like 0/1 Knapsack, this depends on the value of amount, so check that it is small.

60-Second Pitch

"The brute force tries every sequence of coins, which grows exponentially with the amount. I define dp[w] as the fewest coins that make the amount w. Only dp[0] = 0 is real, and every other amount starts at infinity, which means not reachable yet. For each coin, I loop w upward and set dp[w] to the smaller of its value and dp[w - coin] + 1. The upward loop lets dp[w - coin] already include this coin, so a coin can repeat. There are N coins and the amount as the number of sums, so the time is O(N * amount) and the space is O(amount). If the problem asks for the number of ways, I start with dp[0] = 1 and keep the coins in the outer loop, so each combination is counted once."

Common Questions

Why does the forward loop allow a coin to repeat?

When the loop reaches dp[w], the smaller index w - coin was already updated in this pass, so it may already include the coin. Using it again adds one more copy of the same coin. A downward loop reads the old value, so each coin is used at most once.

Does the order of the two loops matter?

For a minimum, no. For a count, yes. Coins in the outer loop count combinations, where order does not matter, as in Coin Change II (LC 518). Amounts in the outer loop count ordered sequences, as in Combination Sum IV (LC 377).

What are the time and space complexity?

O(N * amount) time and O(amount) space, where N is the number of coins. It is pseudo-polynomial, because it grows with the value of the amount, not with its number of digits.

Why not take the largest coin first?

A greedy choice is wrong for some coin sets. With coins [1, 3, 4] and amount 6, greedy picks 4 + 1 + 1, which is 3 coins, and the best is 3 + 3, which is 2 coins. The table checks every coin for every amount, so it does not make that mistake.

How does Perfect Squares (LC 279) use this?

The items are the squares 1, 4, 9 and so on up to n, each usable any number of times, and the goal is the fewest items that add up to n. It is coin_change with those squares as the coins. For n = 12 the answer is 3, namely 4 + 4 + 4. For n = 13 it is 2, namely 4 + 9.

SWE Reasoning

Spot it

  • You build a target amount from items, and each item can be used any number of times: coins, squares, step sizes.
  • The question asks for the fewest items, or for the number of ways to reach the amount.
  • The amount is small enough to be an array index.
  • The statement says whether the order of the items matters. If it does not, count combinations. If it does, count sequences.
  • A greedy choice of the largest coin fails on some coin sets, so a table is needed. With coins [1, 3, 4] and amount 6, the best answer is 3 + 3, and greedy gives 4 + 1 + 1.

The invariant, step by step

  1. dp starts as infinity for every amount, and dp[0] = 0. Only the amount 0 is reachable before any coin.
  2. The outer loop takes one coin. At this moment dp[w] is correct for all coins before it.
  3. The inner loop visits each amount w from coin up to amount.
  4. The update dp[w] = min(dp[w], dp[w - coin] + 1) reads dp[w - coin], which this pass may already have updated. So the coin can be used again.
  5. When the outer loop ends, dp[w] is the fewest coins that make w with all coins, each any number of times, which is the card's invariant.
  6. The code returns dp[amount], or -1 if it is still infinity.

Traps

  • Loop downward when items can repeat. Each coin is used at most once. coin_change([2], 4) returns -1 instead of 2.
  • Put the amounts in the outer loop to count combinations. Each order is counted separately. For Coin Change II (LC 518) with amount 3 and coins [1, 2] the result is 3 instead of 2.
  • Fill dp with 0 instead of infinity in a minimum problem. Every amount looks reachable with 0 coins. coin_change([2], 3) returns 0 instead of -1.
  • Forget the -1 at the end. If dp[amount] is still infinity, returning it as a number gives a wrong or invalid answer.
  • Use a greedy choice. Taking the largest coin first fails for coins = [1, 3, 4] and amount 6.

Practice

Canonical LeetCode:Coin Change (LC 322)Coin Change II (LC 518)Combination Sum IV (LC 377)Perfect Squares (LC 279)
Theory Context•Dynamic Programming

Builds on Linear DP, which you haven’t finished yet.

P1MediumMental model~5 min speedrun

Unbounded Knapsack (Coin Change, Coin Change II)

The problem

Coin Change (LC 322)(opens leetcode.com)

You're given an array coins of coin denominations, each reusable as many times as you like, and a target amount of money.

Work out the smallest number of coins whose values add up to exactly amount, or return -1 if no combination of the given coins reaches that amount exactly.

Example 1
Input:coins = [1,2,5], amount = 11
Output:3
11 = 5 + 5 + 1

Unbounded Knapsack

items & capacities ➔ capacity loop runs upward so an item can repeat ➔ dp[w]
Spot it when: You build a target amount from items that can each be used any number of times, and you want the fewest items or the number of ways: coins, perfect squares, steps of fixed sizes.
Unbounded Knapsack
Python
1def coin_change(coins: list[int], amount: int) -> int:
2 """Unbounded Knapsack: the fewest coins that make amount, each coin usable any number of times (forward loop).
3 Time: O(N * amount) | Space: O(amount)
4 """
5 # dp: dp[w] is the fewest coins that make amount w; infinity marks w as not yet reachable
6 dp = [float('inf')] * (amount + 1)
7 dp[0] = 0
8 for coin in coins:
9 for w in range(coin, amount + 1):
10 # 1. Reuse This Coin Forward:
11 # The forward loop lets dp[w - coin] already include this coin, so one coin may be used any number of times.
12 dp[w] = min(dp[w], dp[w - coin] + 1)
13 return int(dp[amount]) if dp[amount] != float('inf') else -1
Invariant: After each coin, dp[w] is the fewest coins that make w using only the coins processed so far, each any number of times.

Treat each amount as its own subproblem: dp[w] is the fewest coins that make w, and each coin updates it from dp[w - coin]. Loop w upward, so dp[w - coin] may already include this coin and the coin can repeat. Start from infinity, which marks an amount as not yet reachable, and only dp[0] = 0. The trap is the loop direction: for w in range(coin, amount + 1), not downward, which would use each coin at most once.

See it run

Coin Change, step by step

coin - · w - · dp[amount] inf

coin 1 · w - · dp[amount] inf

coin 1 · w 1 · dp[amount] inf

coin 1 · w 2 · dp[amount] inf

coin 1 · w 3-10 · dp[amount] inf

coin 1 · w 11 · dp[amount] 11

coin 2 · w - · dp[amount] 11

coin 2 · w 2 · dp[amount] 11

coin 2 · w 3 · dp[amount] 11

coin 2 · w 4 · dp[amount] 11

coin 2 · w 5-10 · dp[amount] 11

coin 2 · w 11 · dp[amount] 6

coin 5 · w - · dp[amount] 6

coin 5 · w 5 · dp[amount] 6

coin 5 · w 6 · dp[amount] 6

coin 5 · w 7-9 · dp[amount] 6

coin 5 · w 10 · dp[amount] 6

coin 5 · w 11 · dp[amount] 3

coin - · w - · dp[amount] 3

Row k is the list dp after the k-th coin. Column w is the amount w.

  • write the cell being written
  • read the cell dp[w - coin], which may already include this coin
  • answer the final answer

Start: coins = [1, 2, 5] and amount = 11. dp[0] = 0 because the amount 0 needs no coin. Every other cell is inf, which means the amount cannot be made yet.

Coin 1. The loop runs w upward from 1 to 11. So dp[w - 1] may already include this coin.

w = 1: dp[1] = min(inf, dp[0] + 1) = min(inf, 0 + 1) = 1.

w = 2: dp[2] = min(inf, dp[1] + 1) = min(inf, 1 + 1) = 2. dp[1] was written earlier in this pass, so coin 1 is used again.

w = 3 to 10: the same rule runs on each sum in order. New values: 3, 4, 5, 6, 7, 8, 9, 10.

w = 11: dp[11] = min(inf, dp[10] + 1) = min(inf, 10 + 1) = 11. dp[10] was written earlier in this pass, so coin 1 is used again.

Coin 2. The loop runs w upward from 2 to 11. So dp[w - 2] may already include this coin.

w = 2: dp[2] = min(2, dp[0] + 1) = min(2, 0 + 1) = 1.

w = 3: dp[3] = min(3, dp[1] + 1) = min(3, 1 + 1) = 2.

w = 4: dp[4] = min(4, dp[2] + 1) = min(4, 1 + 1) = 2. dp[2] was written earlier in this pass, so coin 2 is used again.

w = 5 to 10: the same rule runs on each sum in order. New values: 3, 3, 4, 4, 5, 5.

w = 11: dp[11] = min(11, dp[9] + 1) = min(11, 5 + 1) = 6. dp[9] was written earlier in this pass, so coin 2 is used again.

Coin 5. The loop runs w upward from 5 to 11. So dp[w - 5] may already include this coin.

w = 5: dp[5] = min(3, dp[0] + 1) = min(3, 0 + 1) = 1.

w = 6: dp[6] = min(3, dp[1] + 1) = min(3, 1 + 1) = 2.

w = 7 to 9: the same rule runs on each sum in order. New values: 2, 3, 3.

w = 10: dp[10] = min(5, dp[5] + 1) = min(5, 1 + 1) = 2. dp[5] was written earlier in this pass, so coin 5 is used again.

w = 11: dp[11] = min(6, dp[6] + 1) = min(6, 2 + 1) = 3. dp[6] was written earlier in this pass, so coin 5 is used again.

All coins are processed. dp[11] = 3, the fewest coins that make 11.

start
00write
1inf
2inf
3inf
4inf
5inf
6inf
7inf
8inf
9inf
10inf
11inf
coin 1
coin 2
coin 5
1 / 19
Gotchas1 of 3

Gotcha 1 of 3

  1. 1
    The trap

    Gotcha 1: Looping w downward when items can be reused

    Mistake
    for w in range(amount, coin - 1, -1):
    Fix
    for w in range(coin, amount + 1):

    What happens: Going upward, dp[w - coin] may already include this coin, so dp[w] can use it again. That is what 'any number of times' needs. Going downward reads dp[w - coin] before this pass updates it, so each coin is used at most once, which is the 0/1 rule. With coins = [2], amount 4 needs the coin twice.

    Example: coins = [2], amount = 4
    Mistake: returns -1Fix: returns 2
  2. 2

    Gotcha 2: Putting the amounts in the outer loop to count combinations

    Mistake
    for w in range(1, amount + 1):
        for coin in coins:
    Fix
    for coin in coins:
        for w in range(coin, amount + 1):

    What happens: Coin Change II (LC 518) counts combinations, so 1 + 2 and 2 + 1 are one way. With coins in the outer loop, each coin is added after the coins before it, in one order only. With amounts outside, every order is counted as its own way. A counting problem starts from dp[0] = 1, the one way to make 0.

    Example: Coin Change II (LC 518): amount = 3, coins = [1, 2]
    Mistake: returns 3Fix: returns 2
  3. 3

    Gotcha 3: Filling dp with 0 instead of infinity

    Mistake
    dp = [0] * (amount + 1)
    Fix
    dp = [float('inf')] * (amount + 1)

    What happens: dp[w] is the fewest coins for amount w, and infinity means w cannot be made. dp[0] = 0 is the only real start. With 0 everywhere, min keeps 0 for every amount. So an amount that cannot be made looks free.

    Example: coins = [2], amount = 3
    Mistake: returns 0Fix: returns -1
Not this one if…
  • Each item can be used at most once, as in Partition Equal Subset Sum (LC 416): the capacity loop runs downward, so an item cannot fill dp[w - item] and then dp[w] in the same pass. See 0/1 Knapsack
  • You must return every combination itself, not just the best value or the number of ways, as in Combination Sum (LC 39): a table of numbers per amount can't list them. See Combinations

Deep Dive

The state is an amount, and each item may repeat

In an unbounded knapsack problem, each item can be used any number of times. In Coin Change (LC 322), the state is an amount w, and dp[w] is the fewest coins that make w using only the coins processed so far.

The answer type changes with the problem. In coin_change, dp[w] is a minimum. In Coin Change II (LC 518), it is a number of ways. In Perfect Squares (LC 279), the items are the squares 1, 4, 9 and so on, and dp[w] is the fewest squares that add up to w.

After coin k, dp describes the first k coins, each used any number of times. The next coin adds the choice: end with this coin, so dp[w] can become dp[w - coin] + 1.

Example: for coins = [1, 2, 5] and amount 11, the answer is 3, namely 5 + 5 + 1. After the coin 1, dp is [0, 1, 2, ..., 11]. After the coin 2, dp[11] is 6. After the coin 5, dp[11] is 3.

Why the forward loop lets a coin repeat

The inner loop runs w upward from coin. When the loop updates dp[w], the smaller index w - coin was already updated in this pass. So dp[w - coin] may already include this coin, and dp[w] can use it again.

This is exactly what 'any number of times' needs. For coins = [2] and amount 4, dp[2] becomes 1, and then dp[4] reads dp[2] and becomes 2. The answer is 2.

A downward loop would read dp[w - coin] before it is updated, so each coin would be used at most once. That is the rule of 0/1 Knapsack. With a downward loop, coin_change([2], 4) returns -1 instead of 2.

Infinity or 0 for the unreachable start

In a minimum problem, an amount that cannot be made yet must lose every comparison. So dp starts as infinity, and only dp[0] = 0 is real. Infinity marks an amount as not reachable yet.

With 0 everywhere, min keeps 0 for every amount, and an amount that cannot be made looks free. coin_change([2], 3) would return 0 instead of -1.

At the end, the code returns -1 when dp[amount] is still infinity. A counting problem uses a different start: dp[0] = 1, the one way to make 0 with no coin, and 0 for the others.

Which loop is outer when you count

For a minimum, the order of the two loops does not change the result. For a count, it does.

With coins in the outer loop, as in coin_change, each combination is built in the order of the coin list, so it is counted once. This is Coin Change II (LC 518): for amount 3 and coins [1, 2], the answer is 2, namely 1 + 1 + 1 and 1 + 2.

With amounts in the outer loop, 1 + 2 and 2 + 1 are different sequences, and both are counted. This is Combination Sum IV (LC 377), which counts ordered sequences. For nums = [1, 2, 3] and target 4 the answer is 7. The same loop on Coin Change II (LC 518) gives 3 for amount 3, which is wrong there.

The cost is O(N * amount) time and O(amount) space, where N is the number of coins. Like 0/1 Knapsack, this depends on the value of amount, so check that it is small.

60-Second Pitch

"The brute force tries every sequence of coins, which grows exponentially with the amount. I define dp[w] as the fewest coins that make the amount w. Only dp[0] = 0 is real, and every other amount starts at infinity, which means not reachable yet. For each coin, I loop w upward and set dp[w] to the smaller of its value and dp[w - coin] + 1. The upward loop lets dp[w - coin] already include this coin, so a coin can repeat. There are N coins and the amount as the number of sums, so the time is O(N * amount) and the space is O(amount). If the problem asks for the number of ways, I start with dp[0] = 1 and keep the coins in the outer loop, so each combination is counted once."

Common Questions

Why does the forward loop allow a coin to repeat?

When the loop reaches dp[w], the smaller index w - coin was already updated in this pass, so it may already include the coin. Using it again adds one more copy of the same coin. A downward loop reads the old value, so each coin is used at most once.

Does the order of the two loops matter?

For a minimum, no. For a count, yes. Coins in the outer loop count combinations, where order does not matter, as in Coin Change II (LC 518). Amounts in the outer loop count ordered sequences, as in Combination Sum IV (LC 377).

What are the time and space complexity?

O(N * amount) time and O(amount) space, where N is the number of coins. It is pseudo-polynomial, because it grows with the value of the amount, not with its number of digits.

Why not take the largest coin first?

A greedy choice is wrong for some coin sets. With coins [1, 3, 4] and amount 6, greedy picks 4 + 1 + 1, which is 3 coins, and the best is 3 + 3, which is 2 coins. The table checks every coin for every amount, so it does not make that mistake.

How does Perfect Squares (LC 279) use this?

The items are the squares 1, 4, 9 and so on up to n, each usable any number of times, and the goal is the fewest items that add up to n. It is coin_change with those squares as the coins. For n = 12 the answer is 3, namely 4 + 4 + 4. For n = 13 it is 2, namely 4 + 9.

SWE Reasoning

Spot it

  • You build a target amount from items, and each item can be used any number of times: coins, squares, step sizes.
  • The question asks for the fewest items, or for the number of ways to reach the amount.
  • The amount is small enough to be an array index.
  • The statement says whether the order of the items matters. If it does not, count combinations. If it does, count sequences.
  • A greedy choice of the largest coin fails on some coin sets, so a table is needed. With coins [1, 3, 4] and amount 6, the best answer is 3 + 3, and greedy gives 4 + 1 + 1.

The invariant, step by step

  1. dp starts as infinity for every amount, and dp[0] = 0. Only the amount 0 is reachable before any coin.
  2. The outer loop takes one coin. At this moment dp[w] is correct for all coins before it.
  3. The inner loop visits each amount w from coin up to amount.
  4. The update dp[w] = min(dp[w], dp[w - coin] + 1) reads dp[w - coin], which this pass may already have updated. So the coin can be used again.
  5. When the outer loop ends, dp[w] is the fewest coins that make w with all coins, each any number of times, which is the card's invariant.
  6. The code returns dp[amount], or -1 if it is still infinity.

Traps

  • Loop downward when items can repeat. Each coin is used at most once. coin_change([2], 4) returns -1 instead of 2.
  • Put the amounts in the outer loop to count combinations. Each order is counted separately. For Coin Change II (LC 518) with amount 3 and coins [1, 2] the result is 3 instead of 2.
  • Fill dp with 0 instead of infinity in a minimum problem. Every amount looks reachable with 0 coins. coin_change([2], 3) returns 0 instead of -1.
  • Forget the -1 at the end. If dp[amount] is still infinity, returning it as a number gives a wrong or invalid answer.
  • Use a greedy choice. Taking the largest coin first fails for coins = [1, 3, 4] and amount 6.

Practice

Canonical LeetCode:Coin Change (LC 322)Coin Change II (LC 518)Combination Sum IV (LC 377)Perfect Squares (LC 279)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•