Coin Change & Unbounded Knapsack
The previous lesson let each item be taken at most once. Drop that cap — items may be reused any number of times — and the 0/1 knapsack collapses from a two-dimensional table to a one-dimensional one, because there is no longer a prefix of "already-used" items to track.
╌╌╌╌
The previous lesson solved 0/1 knapsack, where each item is taken whole or left
behind, at most once. The 0/1
in the name was the include/exclude bit on
every item, and it forced a two-dimensional table : we had to remember
which prefix of items was still on the table, because once item was used it
could not be used again. Now relax exactly that constraint. Let every item be
available in unlimited supply, so the thief may pack as many copies of item
as fit. This is the unbounded knapsack problem, and the single change of
infinite copies does something surprising to the dynamic program: it erases one
whole dimension of the table.
In 0/1 knapsack the second index existed to stop us from reusing an
item; have I already taken item ?
was genuine state. When items may be
reused without limit, that question is meaningless (the set of available items
never shrinks), so the only thing a subproblem needs to remember is how much
capacity is left. One number, one dimension.
Unbounded knapsack: one dimension instead of two
Define the subproblem on capacity alone:
The answer is (or if we want at most
; padding
with a zero-value, weight-one item makes them equal). To fill , consider the
last item placed into the knapsack. It is some type with ; after
placing it we have value plus the best we can do with the remaining budget
, and crucially that remaining budget may use type again:
Stare at the right-hand side and compare it to 0/1 knapsack's
. There the include branch read
, the previous row, item removed from the pool. Here the
include branch reads , the same , item still in the pool.
That one difference is the whole distinction between use once
and use any number of times.
Because the available-item set never changes, the item loop and the weight loop
may be nested in either order; there is no previous row
to respect, only the
ascending-weight rule. We fill cells, each scanning up to items:
time, the same as 0/1 knapsack, but in space: one array, no second dimension to collapse.
Coin change — minimum coins
The cleanest instance of unbounded knapsack strips the values away, just as subset-sum stripped them from 0/1 knapsack. Fix a set of coin denominations , available in unlimited supply, and an amount . Ask: what is the fewest coins that sum to exactly ?
This is unbounded knapsack with every item's value set to (one coin) and the objective flipped to minimize: we want the smallest count, not the largest value. Let be the minimum number of coins summing to amount :
The pays for the coin we just placed; the over denominations picks the best amount to reach the shortfall . An amount that no combination of coins can hit keeps the sentinel value , which propagates: if every is then so is . The figure below shows the 1-D table filling left to right, each cell reaching back positions for each denomination.
With coins the table reads for amounts : , achieved by (the highlighted transition): two coins, not the three a careless greedy pick would take.
- 1
- 2for to do
- 3unreachable so far
- 4
- 5for to do
- 6if and then
- 7
- 8coin that closed the gap
- 9return: unreachable
The outer loop runs over amounts and the inner over coins, so the running time is in space, pseudo-polynomial in exactly the sense of the previous lesson: polynomial in the numeric value but exponential in its bit length.1
Reconstruction. The value is the coin count; the coins themselves come from the array. Starting at , the denomination is the last coin used, so emit it and jump to ; repeat until .
- 1;
- 2while do
- 3coin that closed
- 4
- 5return
Coin change — counting combinations
A different question on the same coins: not how few coins, but how many distinct ways to make amount . This is Coin Change II, and it counts multisets of coins. and are two ways to make , but and are the same way, because a multiset has no order. Let be the number of such combinations summing to , with (the empty multiset is the one way to make ).
The naive recurrence "" is wrong for combinations; it counts and separately. The fix is structural and is the most famous loop-order subtlety in dynamic programming.
Why does coins-outer kill the double-count? Because each denomination is introduced
exactly once and never revisited, every multiset is built in a fixed canonical
order of denominations (coin first, then , and so on), so each multiset
is reached by exactly one path through the loops. The amount-outer loop, by
contrast, re-asks which coin is last?
at every amount, so the same multiset is
counted once for each ordering of its coins.
Because coin is introduced only after coin is fully folded in, each
multiset is built in the fixed order ones first, then twos,
so counts
and once apiece and never sees a .
- 1;
- 2for to docoins outer: combinations
- 3for to doamount inner, ascending: reuse
- 4
- 5return
Swapping the two loops (for a outside, for i inside) computes
instead, the ordered count (Combination
Sum IV), where and are distinct. Same body, same time;
the nesting alone flips the meaning.
A worked count: combinations vs sequences
Take coins and amount . As an unordered count the answers are and : two combinations. As an ordered count we also distinguish the arrangements of , giving , , and , for three sequences. The figure traces both, and ties each to its loop order.
The blue link makes the discrepancy concrete: the single combination on the left explodes into the two ordered sequences and on the right. Counting the left column is the coins-outer loop; counting the right is amount-outer. Picking the wrong nesting silently answers the wrong question: no crash, just a wrong number, which is what makes it the classic bug.
Why greedy fails — and when it works
Coin change has a seductive greedy heuristic: repeatedly take the largest coin that fits. For the U.S. currency system it always gives the minimum, which is why cashiers can make change without dynamic programming. But it is not correct in general. Take coins and amount . Greedy grabs the , then must finish with , for three coins, yet makes in two. The largest-coin-first choice walked into a remainder () that the denominations cover badly, while a less greedy first step () left a remainder the coins cover perfectly.
A coin system is called canonical when the greedy algorithm is optimal for every amount; standard currencies (like ) are deliberately designed to be canonical so that greedy change-making works. Whether an arbitrary system is canonical is itself a nontrivial question (it can be decided by checking greedy against the DP optimum over a bounded range of amounts), but the safe default for an unknown denomination set is the dynamic program, which is correct for any coins.3
The same shape elsewhere: Perfect Squares and Word Break
Once you see coin change as unbounded knapsack, two famous problems reveal themselves as the identical recurrence with the coins renamed.
Perfect Squares asks for the fewest perfect squares ()
summing to . That is exactly with the coins
being the
squares :
The squares are reusable (you may use four times to make ), so it is the ascending-weight minimization we already wrote; only the denomination set changes.
Word Break asks whether a string can be segmented into dictionary words. The
coins
are now words, the amount
is a string prefix, and the table is indexed
by prefix length. Let be true if the first characters of split into
dictionary words:
It is the boolean () flavor, like subset-sum was to knapsack, where a word
ending at position plays the role of a coin of value
landing the
prefix on the earlier boundary . The empty prefix is the
always-reachable base, exactly like .
Takeaways
- Unbounded knapsack lets each item be used any number of times. That single change deletes the item dimension: the subproblem depends only on remaining capacity, giving the 1-D recurrence in time and space, versus 0/1 knapsack's 2-D .
- The include branch reads at the same item-availability (not the previous row), so we sweep weight ascending to permit reuse, the exact opposite of 0/1's descending sweep, which forbids it.
- Coin change (min coins) is unbounded knapsack with unit values and a objective: , , if unreachable; a array reconstructs the coins.
- Counting ways is governed by loop order: coins outer, amount inner counts unordered combinations (Coin Change II); amount outer, coins inner counts ordered sequences (Combination Sum IV). Same code, different question: the classic bug.
- Greedy (largest coin first) fails in general (, amount : greedy , optimal ) but is correct for canonical systems like standard currency; the DP is correct for any denominations.
- Perfect Squares (squares as coins) and Word Break (dictionary words as coins over string prefixes) are the same unbounded-DP shape.
Footnotes
- Skiena, § — Knapsack / Coin Change: making change as unbounded knapsack, and pseudo-polynomial in the amount. ↩
- Erickson, Ch. — Dynamic Programming: combinations vs. compositions and how the nesting of the item and target loops selects between unordered and ordered counts. ↩
- Skiena, § — Knapsack / Coin Change: greedy change-making is optimal only for canonical denomination systems; the DP is correct for arbitrary coins. ↩
╌╌ END ╌╌