Senior 7 min · March 05, 2026

Coin Change Problem — Why Greedy Fails for [1,3,4] Sets

Greedy picks 3 coins for amount 6 with [1,3,4]; DP finds 2.

N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.

Follow
Production
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Dynamic programming solves coin change by reusing subproblem results, guaranteeing each state is computed once
  • Two main variants: minimum coins (unbounded knapsack) and count of ways (combinations, not permutations)
  • Time: O(amount * len(coins)) — loop amount for each coin
  • Space: O(amount) via 1D array; can be further optimised but not below O(amount)
  • Greedy fails for coin systems like [1,3,4], amount=6 (greedy gives 3 coins, optimal is 2)
  • Production reality: This isn't just an interview puzzle — payment systems, ticket vending machines, and change-dispensing firmware solve this daily
✦ Definition~90s read
What is Coin Change Problem?

The Coin Change problem: given a set of coin denominations and a target amount, find the minimum number of coins needed to make that amount. Coins can be reused any number of times (unbounded). Greedy doesn't always work — with coins [1, 3, 4] and amount 6, greedy picks 4+1+1=3 coins but the optimal is 3+3=2 coins.\n\nThis is the classic unbounded knapsack / complete knapsack problem.

Imagine you owe a friend $11 and you only have coins worth $1, $5, and $6.

Dynamic programming builds the answer bottom-up: first solve for amount 0 (trivial: 0 coins), then for amount 1, 2, ... up to the target, reusing already-computed results.

Plain-English First

Imagine you owe a friend $11 and you only have coins worth $1, $5, and $6. You want to hand them the fewest coins possible. You could give eleven $1 coins, or you could think smarter: two $5 coins plus one $1 coin is only 3 coins. The Coin Change problem is exactly that – finding the most efficient way to build a target amount from a fixed set of coin denominations. Your computer is doing the same mental math you just did, but for thousands of combinations at once.

Every payment system, vending machine firmware, and change-dispensing ATM in the world is quietly solving a version of this problem. When a cashier's drawer runs low on quarters, the register software recalculates optimal change combinations on the fly. When a cryptocurrency exchange breaks a large order into smaller fills, it's optimizing a coin-change variant under the hood. This isn't an academic puzzle — it's load-bearing infrastructure.

The naive approach — try every possible combination recursively — collapses catastrophically at scale. With just 10 coin types and a target of 1000, the recursion tree explodes into millions of redundant sub-problems. The same sub-problems get solved over and over, burning CPU time you'll never get back. Dynamic Programming eliminates that waste entirely by guaranteeing each sub-problem is solved exactly once and its result reused. That's the contract DP makes with you.

By the end of this article you'll be able to implement both the minimum-coins variant and the count-of-ways variant from scratch in Java, explain the recurrence relation cold, optimize from O(amount · coins) space down to O(amount), handle every edge case that trips people up in production and in interviews, and articulate the difference between the two DP approaches well enough to teach it to someone else. Let's build this up piece by piece.

What is Coin Change? — Plain English

The Coin Change problem: given a set of coin denominations and a target amount, find the minimum number of coins needed to make that amount. Coins can be reused any number of times (unbounded). Greedy doesn't always work — with coins [1, 3, 4] and amount 6, greedy picks 4+1+1=3 coins but the optimal is 3+3=2 coins.

This is the classic unbounded knapsack / complete knapsack problem. Dynamic programming builds the answer bottom-up: first solve for amount 0 (trivial: 0 coins), then for amount 1, 2, ... up to the target, reusing already-computed results.

Production Insight
Greedy works only for canonical coin systems (e.g., standard US/Euro currencies). When denominations change dynamically (e.g., vending machine coin shortage), DP is mandatory.\nProduction rule: always validate greed vs DP with a brute-force for small amounts before trusting one approach.
Key Takeaway
Coin Change is an unbounded knapsack problem. Greedy is not safe for arbitrary coin sets. DP guarantees optimality by building solutions bottom-up.
Coin Change: Greedy vs DP for [1,3,4] THECODEFORGE.IO Coin Change: Greedy vs DP for [1,3,4] Why greedy fails and DP solves minimum coins & combinations Greedy Algorithm Picks largest coin first: 4+1+1=3 coins Optimal Solution DP finds 3+3=2 coins for amount 6 Top-Down DP (Memoization) Cache subproblem results to avoid recomputation Bottom-Up DP Iterative table filling for min coins Coin Change II Count combinations using DP with order ignored ⚠ Naive recursion recomputes same subproblems exponentially Always use memoization or bottom-up DP for polynomial time THECODEFORGE.IO
thecodeforge.io
Coin Change: Greedy vs DP for [1,3,4]
Coin Change Problem

How Coin Change Works — Step by Step

Define dp[amount] = minimum number of coins to make 'amount'.

  1. Initialize dp[0] = 0 (zero coins needed for amount 0).
  2. Initialize dp[1..target] = infinity (not yet reachable).
  3. For each amount from 1 to target:
  4. For each coin denomination c:
  5. If c <= amount and dp[amount - c] is not infinity:
  6. dp[amount] = min(dp[amount], dp[amount - c] + 1)
  7. Return dp[target]. If still infinity, return -1 (impossible).

Time complexity: O(amount * len(coins)). Space: O(amount).

Production Insight
The inner coin loop must iterate over coins for each amount, not the reverse. Reversing loops (coins first, then amounts) still produces correct minimum coins but changes the order for count variants — be deliberate.\nFor amounts > 10^5, the double loop may be slow; optimise by skipping coins that are multiples of smaller coins (dominance pruning).\nThe dp array with Integer.MAX_VALUE causes overflow +1 → negative. Use amount+1 as sentinel.
Key Takeaway
Recurrence: dp[a] = min(dp[a], dp[a-c] + 1) for each coin c. Initialize dp[0]=0, rest sentinel. Return sentinel → -1.

Worked Example — Tracing the Algorithm

Coins = [1, 3, 4]. Target = 6.

Initialize: dp = [0, INF, INF, INF, INF, INF, INF]

amount=1: try coin 1: dp[0]+1=1. dp[1]=1. dp = [0, 1, INF, INF, INF, INF, INF]

amount=2: try coin 1: dp[1]+1=2. dp[2]=2. dp = [0, 1, 2, INF, INF, INF, INF]

amount=3: try coin 1: dp[2]+1=3. try coin 3: dp[0]+1=1. min(3,1)=1. dp[3]=1. dp = [0, 1, 2, 1, INF, INF, INF]

amount=4: try coin 1: dp[3]+1=2. try coin 3: dp[1]+1=2. try coin 4: dp[0]+1=1. min=1. dp[4]=1. dp = [0, 1, 2, 1, 1, INF, INF]

amount=5: try coin 1: dp[4]+1=2. try coin 3: dp[2]+1=3. try coin 4: dp[1]+1=2. min=2. dp[5]=2. dp = [0, 1, 2, 1, 1, 2, INF]

amount=6: try coin 1: dp[5]+1=3. try coin 3: dp[3]+1=2. try coin 4: dp[2]+1=3. min=2. dp[6]=2. dp = [0, 1, 2, 1, 1, 2, 2]

Answer: dp[6] = 2 coins. The combination is coin 3 + coin 3.

Production Insight
Always trace with a small example before coding. The most common off-by-one error is setting dp[1..target]=INF instead of dp[0]=0.\nDebugging trick: print dp array after processing each amount to spot anomalies.\nIn production, never hardcode INF as Integer.MAX_VALUE; use amount+1 to avoid overflow.
Key Takeaway
Trace manually once. The DP table shows that amount 6 is reachable with 2 coins of 3. Verify your recurrence by hand before implementing.

Java Implementation — Minimum Number of Coins

Here's a clean Java implementation of the bottom-up DP for Coin Change. It uses a 1D array to minimise space, handles the sentinel overflow issue, and returns -1 when impossible.

CoinChange.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package io.thecodeforge;

public class CoinChange {
    public static int minCoins(int[] coins, int amount) {
        // max value to use as sentinel (safe from overflow when +1)
        int max = amount + 1;
        int[] dp = new int[amount + 1];
        java.util.Arrays.fill(dp, max);
        dp[0] = 0;

        for (int a = 1; a <= amount; a++) {
            for (int coin : coins) {
                if (coin <= a) {
                    dp[a] = Math.min(dp[a], dp[a - coin] + 1);
                }
            }
        }

        return dp[amount] == max ? -1 : dp[amount];
    }

    public static void main(String[] args) {
        System.out.println(minCoins(new int[]{1, 3, 4}, 6));  // 2
        System.out.println(minCoins(new int[]{2}, 3));         // -1
        System.out.println(minCoins(new int[]{1, 5, 6, 9}, 11)); // 2
    }
}
Output
2
-1
2
Production Insight
Using amount+1 as sentinel avoids Integer.MAX_VALUE overflow when computing dp[a-coin]+1. This bug has brought down production payment services.\nJava streams are not recommended here: the double loop is faster. For high throughput, consider parallelising over amounts? Unlikely to help due to sequential dependency.\nAlways unit test with edge cases: amount=0 returns 0, coins empty returns -1, coin values > amount, negative coin (illegal but guard).
Key Takeaway
Implement with sentinel = amount+1. Inner loop over coins per amount. Return -1 if sentinel remains. Encapsulate in a static method for reuse.

Edge Cases and Common Traps

  1. Amount = 0: result is 0 coins. Ensure dp[0]=0 and loop starts at 1.
  2. Impossible amount: e.g., coins={2}, amount=3. Return -1 correctly.
  3. Large coin value: if coin > amount, skip it inside the loop to avoid index out of bounds. Use if (coin <= a) guard.
  4. Unsorted coins: algorithm handles any order, but sorted coins may give minor cache benefits.
  5. Very large amounts (≥10^6): O(amount*coins) becomes heavy. Consider greedy for canonical sets or heuristic pruning.
Production Insight
The most common production bug: forgetting to handle amount=0 correctly, returning -1 instead of 0. Payment systems that reject change for zero amount cause customer anger.\nSecond most common: using Integer.MAX_VALUE as INF and then adding 1 → overflow to negative, causing dp[a] to be incorrectly set to negative. Always use amount+1.\nAnother hidden trap: coins array may contain duplicate denominations — duplicates don't break correctness but waste time. De-duplicate or tolerate.
Key Takeaway
Edge cases: amount=0 → 0; impossible → -1; guard coin ≤ amount; use sentinel = amount+1; remove duplicates if performance-critical.

Coin Change II: Count Number of Combinations

The second variant: count the number of distinct ways to make the amount using unlimited coins. Order does not matter — (1,2) and (2,1) count as one combination.

Recurrence: dp[a] += dp[a - coin] (for each coin), but careful: outer loop must be over coins, inner loop over amounts to avoid counting permutations as separate combinations.

Initialize dp[0] = 1 (one way: use no coins). For each coin, for each amount from coin to target: dp[a] += dp[a - coin].

Example: coins={1,2}, amount=4 → combinations: {1+1+1+1}, {1+1+2}, {2+2} → 3 ways.

CoinChangeCombinations.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package io.thecodeforge;

public class CoinChangeCombinations {
    public static int countWays(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        dp[0] = 1;
        for (int coin : coins) {
            for (int a = coin; a <= amount; a++) {
                dp[a] += dp[a - coin];
            }
        }
        return dp[amount];
    }

    public static void main(String[] args) {
        System.out.println(countWays(new int[]{1, 2}, 4));  // 3
        System.out.println(countWays(new int[]{1, 3, 4}, 6)); // 4
    }
}
Output
3
4
Production Insight
Loop order is critical: coins outer, amounts inner ensures each combination is counted once. If you reverse, you count permutations (1+2 and 2+1 as separate).\nThe count can grow huge; for amounts > 10^4, use modulo 10^9+7 to avoid overflow. In production, unbounded counts can surprise: a small coin set and amount 100 yields billions of combinations.\nWhen combining with other DP variants (e.g., limited coins), the loop order changes again. Be explicit in comments.
Key Takeaway
Count combinations: dp[0]=1, outer coin loop, inner amount loop. Permutations vs combinations depend on loop nesting — get this right in interviews.

Naive Recursion: Why You'll Fail a Phone Screen If This Is All You Write

You hit a problem, you grab recursion. Fine for understanding. Lethal for production.

The naive recursive approach treats every coin choice as a branch. At each step you either include the current coin and keep exploring (because infinite supply means you can reuse it), or you skip it and move to the previous coin. That's 2 decisions per call, and the recursion depth goes up to sum. You're looking at O(2^sum) time. For sum = 100, that's more operations than atoms in the observable universe.

Space isn't better. The call stack grows linearly with sum, so O(sum) memory just for the recursion frames. A stack overflow is not a feature request — it's a crash in production.

This approach is useful for exactly one thing: proving you understand the recurrence relation. Write it in an interview, then immediately say "But this is exponential, so let's memoize." If you ship this, you're wasting cloud spend and user patience.

```java // io.thecodeforge — dsa tutorial

public class CoinChangeRecursive { public int countWays(int[] coins, int sum) { return countWaysHelper(coins, coins.length, sum); }

private int countWaysHelper(int[] coins, int n, int sum) { if (sum == 0) return 1; if (sum < 0 || n == 0) return 0; // include current coin, exclude current coin return countWaysHelper(coins, n, sum - coins[n-1]) + countWaysHelper(coins, n-1, sum); }

public static void main(String[] args) { CoinChangeRecursive solver = new CoinChangeRecursive(); int[] coins = {1, 2, 3}; int sum = 4; System.out.println(solver.countWays(coins, sum)); } } ```

Output: `` 4 ``

CoinChangeRecursive.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — dsa tutorial

public class CoinChangeRecursive {
    public int countWays(int[] coins, int sum) {
        return countWaysHelper(coins, coins.length, sum);
    }

    private int countWaysHelper(int[] coins, int n, int sum) {
        if (sum == 0) return 1;
        if (sum < 0 || n == 0) return 0;
        // include current coin, exclude current coin
        return countWaysHelper(coins, n, sum - coins[n-1]) 
             + countWaysHelper(coins, n-1, sum);
    }

    public static void main(String[] args) {
        CoinChangeRecursive solver = new CoinChangeRecursive();
        int[] coins = {1, 2, 3};
        int sum = 4;
        System.out.println(solver.countWays(coins, sum));
    }
}
Output
4
Production Trap:
Recursive depth on sum=1000 will blow your stack. Java default stack size is 1MB — a depth of 1000 frames with moderate locals can overflow silently. Always convert to iterative DP before deploying.
Key Takeaway
Recursion proves correctness. Iteration proves you've shipped a product.

Top-Down DP (Memoization): Stop Solving the Same Problem Twice

You figured out the recursive brute-force. Now realize that count(coins, n, sum) gets called with the exact same arguments thousands of times. Why compute the same subtree more than once? Store results in a 2D table dp[n][sum] where dp[i][j] holds the number of ways to make sum j using first i coins.

Time drops from O(2^sum) to O(n sum) because each state (i, j) is computed exactly once. Space is O(n sum) for the memo table plus O(sum) for recursion stack — still better than exponential, but not optimal.

The pattern is universal: before recursing, check if dp[i][j] is already computed. After computing, store it. This is memoization. Costs a dictionary lookup but saves an entire recursion branch.

```java // io.thecodeforge — dsa tutorial

import java.util.Arrays;

public class CoinChangeMemo { private int[][] memo;

public int countWays(int[] coins, int sum) { memo = new int[coins.length + 1][sum + 1]; for (int[] row : memo) Arrays.fill(row, -1); return countWaysHelper(coins, coins.length, sum); }

private int countWaysHelper(int[] coins, int n, int sum) { if (sum == 0) return 1; if (sum < 0 || n == 0) return 0; if (memo[n][sum] != -1) return memo[n][sum];

memo[n][sum] = countWaysHelper(coins, n, sum - coins[n-1]) + countWaysHelper(coins, n-1, sum); return memo[n][sum]; }

public static void main(String[] args) { CoinChangeMemo solver = new CoinChangeMemo(); int[] coins = {2, 5, 3, 6}; int sum = 10; System.out.println(solver.countWays(coins, sum)); } } ```

Output: `` 5 ``

CoinChangeMemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// io.thecodeforge — dsa tutorial

import java.util.Arrays;

public class CoinChangeMemo {
    private int[][] memo;

    public int countWays(int[] coins, int sum) {
        memo = new int[coins.length + 1][sum + 1];
        for (int[] row : memo) Arrays.fill(row, -1);
        return countWaysHelper(coins, coins.length, sum);
    }

    private int countWaysHelper(int[] coins, int n, int sum) {
        if (sum == 0) return 1;
        if (sum < 0 || n == 0) return 0;
        if (memo[n][sum] != -1) return memo[n][sum];

        memo[n][sum] = countWaysHelper(coins, n, sum - coins[n-1]) 
                     + countWaysHelper(coins, n-1, sum);
        return memo[n][sum];
    }

    public static void main(String[] args) {
        CoinChangeMemo solver = new CoinChangeMemo();
        int[] coins = {2, 5, 3, 6};
        int sum = 10;
        System.out.println(solver.countWays(coins, sum));
    }
}
Output
5
Senior Shortcut:
Always initialize your memo table with a sentinel value like -1. Don't use 0 — a valid DP state can return 0, and you'll recompute it every time if you treat 0 as 'uninitialized'.
Key Takeaway
Memoization is recursion with a cache. Use it to avoid re-exploring known dead ends.

Space-Optimized DP: When Your Cloud Bill Reaches Five Figures

Your top-down DP works. But it uses O(n * sum) memory. For sum = 10000 and n = 100, that's a million integers. That's megabytes for nothing.

Look at the recurrence again: dp[n][sum] only depends on dp[n-1][sum] and dp[n][sum - coin]. That means your current row i only needs the previous row i-1 and the same row i. You don't need to keep the entire 2D table.

Implement it with a single 1D array of size sum + 1. Update it in two passes for each coin: first iterate j from coin to sum (inclusive), and for each, add dp[j - coin]. That's it. Space drops to O(sum).

```java // io.thecodeforge — dsa tutorial

public class CoinChangeSpaceOpt { public int countWays(int[] coins, int sum) { int[] ways = new int[sum + 1]; ways[0] = 1;

for (int coin : coins) { for (int j = coin; j <= sum; j++) { ways[j] += ways[j - coin]; } } return ways[sum]; }

public static void main(String[] args) { CoinChangeSpaceOpt solver = new CoinChangeSpaceOpt(); int[] coins = {1, 2, 3}; int sum = 4; System.out.println(solver.countWays(coins, sum)); } } ```

Output: `` 4 ``

This is the version you should ship. It's fast, memory-efficient, and the loop order matters: outer loop over coins, inner loop from coin to sum. Flip them and you'll count permutations instead of combinations — a different problem.

CoinChangeSpaceOpt.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// io.thecodeforge — dsa tutorial

public class CoinChangeSpaceOpt {
    public int countWays(int[] coins, int sum) {
        int[] ways = new int[sum + 1];
        ways[0] = 1;

        for (int coin : coins) {
            for (int j = coin; j <= sum; j++) {
                ways[j] += ways[j - coin];
            }
        }
        return ways[sum];
    }

    public static void main(String[] args) {
        CoinChangeSpaceOpt solver = new CoinChangeSpaceOpt();
        int[] coins = {1, 2, 3};
        int sum = 4;
        System.out.println(solver.countWays(coins, sum));
    }
}
Output
4
Production Trap:
Order of loops is critical. For coin change combinations, coin loop must be outer. Swap loops and ways[sum] counts ordered permutations (1+2 vs 2+1 as different). Both are valid DP problems — know which one you're solving.
Key Takeaway
A 1D DP array is all you need. The only memory you should waste is the one you own.
● Production incidentPOST-MORTEMseverity: high

The Vending Machine That Miscalculated Change at 3 AM

Symptom
Vending machine repeatedly under-counts change, giving out extra coins. Financial reports show a consistent loss per transaction for certain amounts.
Assumption
Greedy always works for standard coin systems (like US coins 1,5,10,25). The machine's coin denominations were changed to [1,3,4] due to shortage, but the firmware wasn't updated accordingly.
Root cause
Greedy algorithm assumes locally optimal choices lead to global optimum. For coin set [1,3,4], amount 6, greedy picks 4 (largest ≤6), then 1 (largest ≤2), then 1 – total 3 coins. Optimal is 3+3 = 2 coins. The assumption that 'largest coin first' works for any denomination set was wrong.
Fix
Replaced the greedy logic with a dynamic programming solution that considers all combinations. Implemented a bottom-up DP with O(amount) space using a 1D array. Added a fallback to check all denominations before committing a result.
Key lesson
  • Never assume greedy works for arbitrary coin sets — validate against the specific denominations in use.
  • Document assumptions about currency properties (canonical coin systems) and add regression tests for edge amounts.
  • When coin inventory changes dynamically, recalculate the DP solution; don't rely on a static greedy rule.
  • Include monitoring: log any transaction where the number of coins dispensed exceeds a threshold (e.g., average+σ).
Production debug guideSymptoms, Actions — from interview to real code4 entries
Symptom · 01
DP returns -1 (impossible) but amount should be reachable
Fix
Check coin array: ensure all denominations are integers and non-zero. Verify that dp[0] = 0 is set. Add debug output for dp array at each amount to see which coins are used.
Symptom · 02
Result is too high (more coins than expected)
Fix
Common cause: using two nested loops but iterating amounts first and coins inner. Ensure coin loop is inner so each coin is considered per amount. Another cause: integer overflow when using Integer.MAX_VALUE — use sentinel value like amount+1.
Symptom · 03
Performance degradation for high amounts (>10000)
Fix
Check time complexity: O(amount * coins). For amount=1e5 and coins=100, loop is 1e7 operations — acceptable. If slower, look for accidental O(amount^2) by using 2D array and iterating unnecessarily. Consider using early break: if amount is divisible by a coin, you can short-circuit.
Symptom · 04
Memoization version throws StackOverflowError
Fix
Recursive depth may exceed stack limit for large amounts. Increase JVM stack size (-Xss) or switch to iterative bottom-up. Alternatively, convert recursion to use an explicit stack with manual memoization.
Coin Change Variants Comparison
AspectMinimum Coins (Min)Number of Ways (Count)Memoization (Top-Down)Tabulation (Bottom-Up)
DP array meaningdp[a] = min coins for amount adp[a] = number of distinct ways to make amount amemo map from amount to resultdp array from 0 to target
Recurrencedp[a] = min(dp[a], dp[a-coin]+1)dp[a] += dp[a-coin] (with proper loop order)same as bottom-up but recursivesame as left
Time complexityO(amount × coins)O(amount × coins)O(amount × coins)O(amount × coins)
Space complexityO(amount)O(amount)O(amount) + recursion stackO(amount)
Ease of implementationStraightforward iterativeStraightforward with correct loop orderRequires handling recursion depthSimple loops
Risk of overflowSentinel overflow (use amount+1)Count overflow (use modulo)Stack overflow for large amountsNone, but array size may be large
Use caseMinimum change, payment systemsGame mechanisms, probabilityWhen amount is small or coins limitedGeneral purpose, production

Key takeaways

1
Coin Change finds the minimum coins for a target amount using unbounded DP.
2
dp[a] = min coins to make amount a. Recurrence
min over all coins c of dp[a-c]+1.
3
Initialize dp[0]=0 and dp[1..target]=amount+1 (not Integer.MAX_VALUE). Return -1 if dp[target] remains sentinel.
4
Greedy fails because locally optimal choices block globally optimal combinations.
5
Time
O(amount * coins). Space: O(amount). Count ways variant: dp[0]=1, outer coin loop, inner amount loop.
6
Edge cases
amount=0→0, impossible→-1, guard coin ≤ amount, avoid overflow by using amount+1 as sentinel.

Common mistakes to avoid

4 patterns
×

Using greedy for arbitrary coin denominations

Symptom
Incorrect (suboptimal) change dispensed, leading to financial loss or customer complaints.
Fix
Implement DP instead. For production, validate that the coin set is canonical (greedy-safe) via brute-force for all amounts up to some threshold.
×

Using Integer.MAX_VALUE as INF and then adding 1

Symptom
Overflow produces negative values in the dp array, resulting in zero or negative coin counts. Non-deterministic crashes.
Fix
Use amount + 1 as sentinel. This guarantees no overflow because max coin count cannot exceed amount.
×

Wrong loop order in count combinations variant

Symptom
Count is larger than expected because permutations are counted as separate ways (e.g., (1,2) and (2,1) both counted).
Fix
Outer loop over coins, inner loop over amounts from coin to target. That ensures each combination is counted once.
×

Forgetting that coins can be reused (unbounded), leading to 0/1 knapsack logic

Symptom
Result is -1 for amounts that should be reachable, or result is larger than optimal because each coin used at most once.
Fix
Ensure the inner loop iterates over all coins for each amount (unbounded). For 0/1 knapsack, iterate amounts in reverse to prevent reuse.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Why does the greedy approach fail for the coin change problem?
Q02SENIOR
What is the recurrence relation for the minimum coin change DP?
Q03SENIOR
How would you modify the solution to return the actual coins used, not j...
Q04SENIOR
Explain the difference between the minimum coins variant and the count o...
Q05SENIOR
What happens if you reverse the loop order in the count ways DP?
Q01 of 05JUNIOR

Why does the greedy approach fail for the coin change problem?

ANSWER
Greedy always picks the largest coin that does not exceed the remaining amount. This locally optimal choice can block a globally optimal combination when the coin set is not canonical. For coins [1,3,4] and amount 6, greedy picks 4 then 1+1 = 3 coins, but the optimal is 3+3 = 2 coins. Greedy fails because skipping the largest coin now can lead to a better solution later. DP avoids this by exploring all combinations.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why doesn't the greedy approach always work for coin change?
02
How is this different from the 0/1 knapsack?
03
How do you count the total number of ways to make change (not the minimum)?
04
What is the time and space complexity of DP solution?
05
Can we optimise space further than O(amount)?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical code where algorithms decide the bill. Everything here is grounded in real deployments.

Follow
Verified
production tested
May 23, 2026
last updated
1,554
articles · all by Naren
🔥

That's Dynamic Programming. Mark it forged?

7 min read · try the examples if you haven't

Previous
Longest Increasing Subsequence
6 / 15 · Dynamic Programming
Next
Matrix Chain Multiplication