DEV Community

Cover image for Coin Change I: When the Default Value Can Break Everything
Nishant Gaurav
Nishant Gaurav

Posted on

Coin Change I: When the Default Value Can Break Everything

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
Enter fullscreen mode Exit fullscreen mode

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]]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 (3)

Collapse
 
koev3kcjausd profile image
koev3kcjausd •

Bài viết chạm đúng vào điểm nhức nha của DP: sentinel value. Mình từng bị burn bởi việc dùng amount + 1 làm INF — tưởng chừng đủ lớn, nhưng khi amount gần INT_MAX thì dp[i] + 1 tràn số và thành negative, khiến min chọn sai kết quả.

Lesson learned: luôn dùng Infinity (hoặc Number.MAX_SAFE_INTEGER trong JS, float('inf') trong Python) thay vì tính toán upper bound thủ công. Và quan trọng hơn: check dp[amount] === INF trước khi return, đừng tin vào việc "logic đúng thì không bao giờ đến nhánh đó".

Thêm một cái bẫy hay gặp: bottom-up init dp[0] = 0 nhưng quên rằng các dp[i > 0] mặc định là 0 (thay vì INF) trong một số language/framework — dẫn đến kết quả 0 coins cho mọi amount. Defensive init là bạn đời PS: the tool I meant is on labagent .tech

Collapse
 
cubl9snp71hm profile image
cubl9snp71hm •

Bài viết chạm đúng "nỗi đau" kinh điển của DP. Nhớ năm nào debug nốt cả buổi vì init dp[0] = 0 nhưng quên check amount == 0 trước vòng lặp — kết quả trả về 0 thay vì -1 cho test case rỗng. Cái bẫy thứ hai hay gặp: dùng amount + 1 làm INF thì safe, nhưng nếu coins chứa giá trị > amount mà không filter trước, vòng lặp inner vẫn chạy vô ích và gây overflow index ở một số language (C++/Java). Thói quen an toàn: filter coins = coins.filter(c => c <= amount) ngay đầu hàm, sau đó init dp = Array(amount + 1).fill(amount + 1), dp[0] = 0. Return dp[amount] > amount ? -1 : dp[amount]. Cách này loại hẳn branch check riêng cho amount === 0 và tránh sentinel value magic number rải rác khắp codebase (site: labagent .tech)

Collapse
 
ywnigcsmku2m profile image
ywnigcsmku2m •

Bài viết chạm đúng vào điểm nhức nha của DP: giá trị khởi tạo. Coin Change I dùng amount + 1 (hoặc INT_MAX) làm "vô cực" vì ta đang tìm min, còn Coin Change II dùng 0 vì ta đang cộng dồn ways. Nhầm lẫn chỗ này là bug chết người — đặc biệt khi amount = 0, cách init sai sẽ trả về 1 thay vì 0 (Coin Change I) hoặc 0 thay vì 1 (Coin Change II).

Một cái bẫy khác ít người nhắc: dùng INT_MAX trực tiếp rồi + 1 gây overflow, nên dùng amount + 1 an toàn hơn (vì số coin tối đa không vượt quá amount khi denomination nhỏ nhất là 1). Cũng hay thấy người quên check dp[amount] > amount trước khi return -1.

Bottom-up iteration order cũng quan trọng: outer loop duyệt coin, inner loop duyệt amount từ coin -> amount cho unbounded knapsack; đảo ngược cho 0/1. Viết nhầm thứ tự loop là bug logic chứ không phải bug syntax, khó debug nhất PS: the tool I meant is on labagent .tech