Coin Change II counts ways. Coin Change I asks a different question: what is the minimum number of coins needed to make a given amount?
The choice structure is identical. Unbounded, same-row TAKE, previous-row SKIP. Two things change: + becomes min(...) + 1, and the initialisation needs careful thought because the default value for an impossible state can no longer be zero.
That second point is where most people get confused on this problem. This chapter addresses it directly.
The Problem
coins = [1, 2, 3]
amount = 5
Some combinations:
2 + 3 → 2 coins
1 + 1 + 3 → 3 coins
1 + 2 + 2 → 3 coins
1 + 1 + 1 + 2 → 4 coins
Minimum = 2
Same coins as Coin Change II. Same amount. But instead of counting four ways, you want the one that uses the fewest coins.
Why the Initialisation Is Different
In Coin Change II, the table stored counts. A count of zero meant "zero ways to make this amount," which was a valid meaningful answer. And T[i][0] = 1 because there is one way to make amount zero: choose nothing.
Here the table stores the minimum number of coins. Zero means "zero coins needed," which is the correct answer for amount zero. But for amounts that are genuinely impossible to make with the available coins, you can't store zero — that would look like "zero coins needed," which is wrong. You need a sentinel value that means "this state is unreachable."
That sentinel is a very large number, conventionally called INF.
When min() compares a real answer against INF, INF loses and gets ignored. That's its entire purpose.
Why INF = 2**31 - 2 and Not 2**31 - 1
When you TAKE a coin, the transition adds 1:
1 + T[i][j - coins[i-1]]
If T[i][j - coins[i-1]] is 2**31 - 1 (the maximum integer), adding 1 causes integer overflow in languages like C++ and Java. The result wraps around to a negative number, which then looks like a valid minimum — a completely silent wrong answer.
Using 2**31 - 2 keeps it one step back:
1 + (2**31 - 2) = 2**31 - 1
Still a huge number, still loses to any real answer in min(), but doesn't overflow.
In Python, integers don't overflow, so float('inf') works fine. But knowing why the -1 is there matters if you're writing this in any other language.
The DP State
T[i][j] = minimum coins needed to make amount j
using the first i coins
Initialisation in Three Parts
Amount zero: Zero coins needed regardless of which coins are available.
for i in range(n + 1):
T[i][0] = 0
No coins available (row 0): Every positive amount is impossible.
# T[0][j] = INF for all j > 0
# Already handled if table is initialised to INF
First coin row (row 1): This needs its own loop. With only the first coin available, an amount is reachable only if it's exactly divisible by that coin.
for j in range(1, amount + 1):
if j % coins[0] == 0:
T[1][j] = j // coins[0]
else:
T[1][j] = INF
For example, with coins[0] = 3:
Amount: 0 1 2 3 4 5 6 7 8 9
Coins: 0 INF INF 1 INF INF 2 INF INF 3
Amount 3 needs 1 coin. Amount 6 needs 2. Amount 1, 2, 4, 5, 7, 8 are impossible with only coin 3.
The Transition
When the coin fits (coins[i-1] <= j):
T[i][j] = min(
1 + T[i][j - coins[i-1]], # TAKE: same row + 1 coin
T[i-1][j] # SKIP: previous row
)
The +1 counts the coin being taken. The same-row reference allows taking the same coin again.
When the coin doesn't fit:
T[i][j] = T[i-1][j] # only SKIP
The Operation Table
Across the Unbounded family, the operation is the only thing that varies:
| Problem | Goal | Operation |
|---|---|---|
| Rod Cutting | Maximum profit | max(take, skip) |
| Coin Change II | Count ways | take + skip |
| Coin Change I | Minimum coins | min(1 + take, skip) |
The +1 in Coin Change I is because taking a coin costs one coin. The 1 + is absent in Rod Cutting and Coin Change II because they don't count individual items used.
The Complete Code
class Solution:
def coinChange(self, coins, amount):
n = len(coins)
# INF represents an impossible/unreachable state.
# Using 2**31 - 2 instead of 2**31 - 1 so that
# 1 + INF doesn't overflow in other languages.
# In Python, float('inf') also works.
INF = 2**31 - 2
# T[i][j] = minimum coins to make amount j
# using first i coins.
# Start everything as INF — unreachable by default.
T = [[INF] * (amount + 1) for _ in range(n + 1)]
# Amount 0 needs 0 coins — for every row
for i in range(n + 1):
T[i][0] = 0
# First coin row: only reachable if amount
# is exactly divisible by coins[0]
for j in range(1, amount + 1):
if j % coins[0] == 0:
T[1][j] = j // coins[0]
else:
T[1][j] = INF
# Fill remaining rows
for i in range(2, n + 1):
for j in range(1, amount + 1):
if coins[i - 1] <= j:
# TAKE: same row (coin can be reused), add 1 coin
# SKIP: previous row
# min() because we want fewest coins
T[i][j] = min(
1 + T[i][j - coins[i - 1]],
T[i - 1][j]
)
else:
# Coin too large — only skip
T[i][j] = T[i - 1][j]
# If still INF, the amount is impossible
if T[n][amount] == INF:
return -1
return T[n][amount]
The final check if T[n][amount] == INF: return -1 handles the case where no combination of coins can make the target amount. LeetCode's Coin Change problem expects -1 for this case.
One Correction Worth Keeping in Your Notes
An empty coin array with a positive amount does not mean "infinite ways." It means "impossible." There are zero ways, and the minimum number of coins is undefined — represented as INF. The confusion comes from mixing up Count of Subsets thinking (where T[i][0] = 1 represents the empty combination) with this problem (where T[i][0] = 0 represents zero coins needed and INF represents unreachable).
These are different kinds of "nothing."
Quick Revision
T[i][j] = minimum coins to make amount j using first i coins
Initialisation:
Everything starts as INF
T[i][0] = 0 (amount 0 → 0 coins)
T[1][j] = j//coins[0] if j % coins[0] == 0, else INF
Transition:
if coins[i-1] <= j:
T[i][j] = min(1 + T[i][j-coins[i-1]], T[i-1][j])
else:
T[i][j] = T[i-1][j]
Final check:
if T[n][amount] == INF: return -1
Why INF = 2**31 - 2:
1 + INF must not overflow → use INF - 1 as sentinel
What You Now Understand
Coin Change I completes the core Unbounded Knapsack problem family. Three problems, one transition direction, three different operations: max, +, min(...) + 1. The initialisation is the part that actually differs meaningfully between them, because it depends on what the table stores and what "impossible" means in each context.
The next chapter moves to a different DP pattern entirely: Longest Common Subsequence. The choice diagram changes, the table dimensions change, but the same fundamental approach from Chapter 1 applies: understand the choices, find the base case, write the recursion, then build the table.



Top comments (2)
The amount+1 sentinel works fine until you hit integer overflow in languages without arbitrary precision — seen this bite people porting Python solutions to Go/Java where
amount + 1wraps silently. Safer to useamount + 1only when you've validatedamount < MAX_INT, otherwise a dedicated booleandparray orOptional<int>makes the "unreachable" state explicit without relying on a magic number that could theoretically be a valid answer.Also worth noting: the bottom-up loop order matters more than folks realize. Iterating coins outer / amount inner gives you combinations (Coin Change II), but amount outer / coins inner gives you permutations — which is wrong for minimum coins if you have duplicate denominations. Seen that trap catch people who blindly copy the "unbounded knapsack" template — found it via LabAgent, site: labagent .tech
Good points! The sentinel choice is definitely something to be careful with when moving between languages. And yeah, loop order can be confusing when switching between Coin Change I and II. Though for minimum coins, the order of the coin and amount loops doesn't change the result in the same way it does for counting combinations vs permutations. Appreciate you adding this!
Some comments may only be visible to logged-in visitors. Sign in to view all comments.