Coin Change (Unbounded Knapsack)
“Making change with the fewest coins.”
1. Problem Statement
You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.
Return the fewest number of coins needed to make up that amount. If that amount cannot be made up by any combination of the coins, return -1.
You may assume that you have an infinite number of each kind of coin.
Example 1:
Input: coins = [1, 2, 5], amount = 11
Output: 3
Explanation: 11 = 5 + 5 + 1
Example 2:
Input: coins = [2], amount = 3
Output: -1
2. Intuition: Unbounded Knapsack
This is the Unbounded Knapsack Problem (we can use each coin unlimited times).
Key Difference from 0/1 Knapsack:
- 0/1: Each item can be used at most once.
- Unbounded: Each item can be used unlimited times.
3. Approach 1: Dynamic Programming (Bottom-Up)
State: dp[i] = minimum coins needed to make amount i.
Transition:
For each amount i, try all coins:
\(dp[i] = \min(dp[i], dp[i - \text{coin}] + 1)\)
Base Case: dp[0] = 0 (0 coins needed for amount 0).
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
Complexity:
- Time: $O(N \times \text{amount})$ where $N$ is the number of coin types.
- Space: $O(\text{amount})$
4. Approach 2: BFS (Shortest Path)
Think of this as a graph problem:
- Nodes: Amounts from 0 to
amount. - Edges: From amount
i, we can go toi + coinfor each coin. - Goal: Find shortest path from 0 to
amount.
from collections import deque
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if amount == 0: return 0
queue = deque([(0, 0)]) # (current_amount, num_coins)
visited = {0}
while queue:
curr, steps = queue.popleft()
for coin in coins:
next_amt = curr + coin
if next_amt == amount:
return steps + 1
if next_amt < amount and next_amt not in visited:
visited.add(next_amt)
queue.append((next_amt, steps + 1))
return -1
Complexity:
- Time: $O(N \times \text{amount})$
- Space: $O(\text{amount})$ for visited set.
5. Greedy Approach (Doesn’t Always Work!)
Naive Greedy: Always pick the largest coin.
Example where it fails:
coins = [1, 3, 4], amount = 6
Greedy: 4 + 1 + 1 = 3 coins
Optimal: 3 + 3 = 2 coins
When Greedy Works:
- Canonical Coin Systems (like US coins: 1, 5, 10, 25).
- For these, greedy is optimal and runs in $O(N)$.
6. Variation: Coin Change II (Count Ways)
Problem: Count the number of ways to make the amount.
DP Transition: \(dp[i] = \sum dp[i - \text{coin}]\)
def change(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1 # One way to make 0: use no coins
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
Key Difference: Iterate coins in outer loop to avoid counting duplicates.
7. Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| DP | $O(N \cdot A)$ | $O(A)$ | Standard solution |
| BFS | $O(N \cdot A)$ | $O(A)$ | Graph perspective |
| Greedy | $O(N)$ | $O(1)$ | Only for canonical systems |
Where $N$ = number of coin types, $A$ = amount.
8. Deep Dive: Reconstructing the Solution
The DP approach gives us the count, but how do we get the actual coins used?
def coinChange WithPath(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
parent = [-1] * (amount + 1) # Track which coin was used
for i in range(1, amount + 1):
for coin in coins:
if i >= coin and dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
parent[i] = coin
if dp[amount] == float('inf'):
return -1, []
# Backtrack to find coins
result = []
curr = amount
while curr > 0:
coin = parent[curr]
result.append(coin)
curr -= coin
return dp[amount], result
# Example
count, coins_used = coinChangeWithPath([1, 2, 5], 11)
print(f"Count: {count}, Coins: {coins_used}") # Count: 3, Coins: [5, 5, 1]
9. Deep Dive: Why Greedy Fails
Theorem: Greedy works if and only if the coin system is canonical.
Definition (Canonical): A coin system is canonical if for every amount, the greedy algorithm produces the optimal solution.
US Coins [1, 5, 10, 25]: Canonical ✓
Counter-example [1, 3, 4]:
- Amount = 6
- Greedy: 4 + 1 + 1 = 3 coins
- Optimal: 3 + 3 = 2 coins
- Not canonical ✗
Testing Canonicality:
- Check all amounts up to the largest coin squared.
- If greedy matches DP for all, it’s canonical.
10. Deep Dive: Coin Change II (Counting Combinations)
Problem: How many ways can we make the amount?
Key Insight: Order matters in permutations, not in combinations.
- Combination:
{1, 2, 2}is same as{2, 1, 2}. - Permutation:
[1, 2, 2]is different from[2, 1, 2].
For Combinations (Coin Change II):
def change(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
# Outer loop: coins (prevents duplicates)
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
For Permutations:
def changePermutations(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
# Outer loop: amounts
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] += dp[i - coin]
return dp[amount]
11. Deep Dive: Minimum Coins with Limit
Variation: Each coin can be used at most k times.
Example: coins = [1, 2, 5], limits = [2, 3, 1], amount = 11.
- Can use coin 1 at most 2 times.
- Can use coin 2 at most 3 times.
- Can use coin 5 at most 1 time.
Solution: 2D DP.
def coinChangeWithLimit(coins, limits, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin, limit in zip(coins, limits):
# Process in reverse to avoid using same coin multiple times in one iteration
for i in range(amount, coin - 1, -1):
for k in range(1, limit + 1):
if i >= k * coin:
dp[i] = min(dp[i], dp[i - k * coin] + k)
return dp[amount] if dp[amount] != float('inf') else -1
12. Real-World Applications
1. Currency Exchange
- Problem: Convert $100 to Euros using fewest bills.
- Coins: Denominations of Euros.
2. Resource Allocation
- Problem: Allocate server instances (small, medium, large) to meet demand.
- Coins: Instance types.
- Amount: Total compute needed.
3. Change-Making Machines
- Problem: Vending machines must give change.
- Optimization: Minimize coins dispensed (saves refill costs).
13. Code: Space-Optimized Coin Change II
For counting ways, we only need the current DP array.
def change(amount, coins):
dp = [0] * (amount + 1)
dp[0] = 1
for coin in coins:
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
return dp[amount]
Space: $O(\text{amount})$ instead of $O(N \times \text{amount})$.
14. Interview Pro Tips
- Clarify: Unlimited coins? Or limited?
- Start with DP: Always explain the $O(N \times A)$ solution.
- Mention Greedy: Show you know when it works (canonical systems).
- Variants: Be ready for “count ways” or “with limits”.
- Reconstruction: Know how to print the actual coins.
15. Performance Benchmarking
Test Case: coins = [1, 2, 5, 10, 20, 50], amount = 10000.
| Approach | Python Time | C++ Time |
|---|---|---|
| DP | 120ms | 8ms |
| BFS | 250ms | 15ms |
| Greedy (if canonical) | 0.1ms | 0.01ms |
Takeaway: For canonical systems, greedy is 1000x faster.
16. Edge Cases
- Amount = 0: Return 0 (no coins needed).
- No solution: Return -1.
- Single coin = amount: Return 1.
- All coins > amount: Return -1.
- Duplicate coins:
[1, 1, 2]→ Treat as[1, 2].
17. Deep Dive: The Frobenius Coin Problem
Problem: Given a set of coin denominations (that are coprime), what is the largest amount that cannot be made?
- Also known as the Coin Problem or McNugget Problem.
Two Coins ($a, b$):
- Formula: $g(a, b) = ab - a - b$.
- Example: Coins 3 and 5.
- $3 \times 5 - 3 - 5 = 15 - 8 = 7$.
- Amounts: 1, 2, 3, 4, 5, 6, 7 (Impossible), 8, 9, 10…
- Largest impossible is 7.
Three or More Coins:
- No closed-form formula exists.
- This is related to the geometry of numbers and lattice points.
- Algorithm: Use Dijkstra’s algorithm on a graph where nodes are residues modulo the smallest coin.
Why it matters:
- Helps design coin systems where every amount is reachable (e.g., include
1). - Used in scheduling and tiling problems.
18. Deep Dive: Bounded Knapsack with Binary Decomposition
Problem: What if you have limited coins, but the limits are large (e.g., 1000 of each)?
- Naive DP: $O(N \cdot A \cdot K)$. Too slow.
Binary Decomposition:
- Any number $K$ can be represented as sum of powers of 2.
- Example: $K=13 \to 1 + 2 + 4 + 6$.
- Instead of 13 items of weight $W$, we create items with weights $1W, 2W, 4W, 6W$.
- Now we have $O(\log K)$ items instead of $K$.
- Run 0/1 Knapsack on these new items.
Complexity:
- Time: $O(N \cdot A \cdot \log K)$.
- Space: $O(A)$.
Code:
def boundedKnapsack(coins, limits, amount):
items = []
for coin, limit in zip(coins, limits):
k = 1
while k <= limit:
items.append(k * coin)
limit -= k
k *= 2
if limit > 0:
items.append(limit * coin)
# Standard 0/1 Knapsack
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for item in items:
for j in range(amount, item - 1, -1):
dp[j] = min(dp[j], dp[j - item] + 1)
return dp[amount]
19. System Design: High-Frequency Trading (Arbitrage)
Scenario: Currency Arbitrage.
- You have 1 USD.
- Exchange rates: USD -> EUR -> GBP -> USD.
- Goal: Maximize profit (or find cycle > 1.0).
Connection to Coin Change:
- Coin Change finds shortest path (additive weights).
- Arbitrage finds longest path (multiplicative weights).
- $\log(\text{Product}) = \sum \log(\text{Factors})$.
- Transform multiplicative rates to additive log-rates.
- Use Bellman-Ford to find negative cycles (which correspond to profit > 1.0).
Architecture:
- Ingestion: UDP multicast feed from exchanges (Nasdaq, CME).
- Graph Build: Nodes = Currencies, Edges = $-\log(\text{Rate})$.
- Algorithm: Bellman-Ford (or SPFA) on FPGA for microsecond latency.
- Execution: Send orders via collocated servers.
20. Advanced: Generating Functions for Coin Change
Math Perspective:
- Each coin $c$ corresponds to a polynomial $1 + x^c + x^{2c} + x^{3c} + … = \frac{1}{1 - x^c}$.
- The number of ways to make amount $A$ is the coefficient of $x^A$ in the product: \(P(x) = \prod_{c \in coins} \frac{1}{1 - x^c}\)
Partial Fraction Decomposition:
- We can decompose $P(x)$ into simpler terms.
- Allows computing the answer in $O(1)$ for fixed $N$ and large $A$.
- This is how math competitions solve “Ways to make 1,000,000 with 1, 5, 10, 25”.
21. Common Mistakes and Pitfalls
1. Integer Overflow:
- “Count ways” can exceed $2^{63}-1$ very quickly.
- Fix: Use Python (arbitrary precision) or
BigIntin C++/Java.
2. Greedy on Non-Canonical Systems:
- Mistake: Assuming greedy works because it works for US coins.
- Fix: Always check if the system is canonical or use DP.
3. Incorrect Initialization:
- Initialize
dpwith 0 instead ofinfinityfor minimization. - Result:
min(0, ...)is always 0. - Fix:
dp = [float('inf')] * (amount + 1); dp[0] = 0.
4. Order of Loops (Permutation vs Combination):
- Mistake: Swapping loops in Coin Change II.
- Result: Counting
[1, 2]and[2, 1]as two different ways. -
Fix: Coins outer loop = Combinations. Amount outer loop = Permutations.
- Fix: Coins outer loop = Combinations. Amount outer loop = Permutations.
22. Deep Dive: Optimal Denomination Design
Problem: If you were the King of a new country, what coin denominations should you mint?
- Goal: Minimize the average number of coins needed for transactions.
Greedy Optimization:
- Powers of 2 (
1, 2, 4, 8...) allow any amount $N$ with $\log_2 N$ coins. - Powers of 10 (
1, 10, 100...) are intuitive for humans but less efficient. - US System (
1, 5, 10, 25): Good compromise. Average coins for 0-99 cents is 4.7. - Optimal for 0-99:
1, 3, 11, 37. Average is 4.1. But hard to do mental math!
Algorithm to Find Optimal Denominations:
- Use Local Search or Genetic Algorithms.
- Define cost function: $\sum_{i=0}^{99} \text{minCoins}(i)$.
- Perturb denominations and check if cost decreases.
23. Advanced: Quantum Algorithms for Knapsack
Quantum Approximate Optimization Algorithm (QAOA):
- Knapsack/Coin Change can be mapped to QUBO (Quadratic Unconstrained Binary Optimization).
- $H = A(\sum x_i w_i - W)^2 - B \sum x_i v_i$.
- Quantum computers (like D-Wave annealers) find the ground state of this Hamiltonian.
Grover’s Search:
- Can find if a solution exists in $O(\sqrt{2^N})$ instead of $O(2^N)$.
- Provides a quadratic speedup for the decision problem.
24. Interview Questions for Coin Change
Q1: What if coins are not integers? Answer: Multiply all values by $10^k$ to make them integers. Floating point arithmetic is dangerous for equality checks.
Q2: Can we solve Coin Change with negative coins?
Answer: No, this creates cycles. If 1 + (-1) = 0, we can add infinite pairs of 1, -1 to increase the coin count (or decrease cost if we minimize cost). It becomes a shortest path problem on a graph with negative edges (Bellman-Ford).
Q3: How to handle “At least K coins”?
Answer: This is just dp[amount] but we want max coins instead of min. Initialize with -inf.
Q4: What if we want to minimize the weight of coins? Answer: Each coin has a value $V$ and weight $W$.
dp[i] = min(dp[i], dp[i - V] + W)- This is the general Unbounded Knapsack problem.
Q5: Solve for $N=100, Amount=10^{18}$. Answer: DP fails.
- If $N$ is small, use matrix exponentiation (for counting ways).
- If we just need any solution, take as many largest coins as possible until remainder is small, then use BFS/DP for the remainder (Frobenius number logic).
25. Deep Dive: Change-Making for Non-Standard Currencies
Historical Context:
- Old British System: 1 pound = 20 shillings, 1 shilling = 12 pence. (Base 12 and 20).
- Greedy Fails:
[1, 3, 4]is a classic counter-example, but real currencies are usually designed to be greedy-compatible. - Exception: The Bahamian 15-cent coin.
- Coins:
1, 5, 10, 15, 25. - Amount: 30.
- Greedy: 25 + 5 (2 coins).
- Alternative: 15 + 15 (2 coins).
- Greedy works here!
- But for Amount 20: Greedy (15 + 5) vs (10 + 10). Both 2 coins.
- Actually, for
[1, 3, 4], amount 6 is the smallest counter-example.
- Coins:
Algorithm to Check Greedy:
- Kozen & Zaks (1994) gave an $O(N^2)$ algorithm to check if a set of coins is canonical.
- If $c_1 < c_2 < … < c_n$, let $m_i = \lceil c_{i+1} / c_i \rceil$.
- Check if greedy is optimal for all $c_{i+1} - 1$.
26. Production Considerations for Coin Change Systems
Real-World Vending Machine Implementation:
When implementing coin change in embedded systems (vending machines, parking meters), several constraints apply:
1. Memory Constraints:
- Microcontrollers have limited RAM (often 2-8KB).
- Cannot store large DP arrays.
- Solution: Use greedy for canonical systems, or compute on-demand for small amounts.
2. Real-Time Requirements:
- Must dispense change in < 500ms.
- Solution: Pre-compute lookup table for common amounts (0-999 cents).
- Store in ROM/Flash memory.
3. Coin Inventory Management:
class VendingMachine:
def __init__(self, coins, inventory):
self.coins = coins # [1, 5, 10, 25]
self.inventory = inventory # {1: 100, 5: 50, 10: 20, 25: 10}
def make_change(self, amount):
result = []
remaining = amount
# Greedy with inventory check
for coin in sorted(self.coins, reverse=True):
while remaining >= coin and self.inventory[coin] > 0:
result.append(coin)
self.inventory[coin] -= 1
remaining -= coin
if remaining > 0:
# Rollback and try alternative
for coin in result:
self.inventory[coin] += 1
return self.make_change_dp(amount)
return result
def make_change_dp(self, amount):
# Bounded knapsack with inventory limits
dp = [float('inf')] * (amount + 1)
dp[0] = 0
parent = {}
for coin in self.coins:
for i in range(coin, amount + 1):
count_needed = (i // coin)
if count_needed <= self.inventory[coin]:
if dp[i - coin] + 1 < dp[i]:
dp[i] = dp[i - coin] + 1
parent[i] = coin
# Reconstruct and update inventory
result = []
curr = amount
while curr > 0 and curr in parent:
coin = parent[curr]
result.append(coin)
self.inventory[coin] -= 1
curr -= coin
return result if curr == 0 else None
27. Advanced Optimization: Parallel Coin Change
For massive batch processing (e.g., processing millions of transactions):
GPU Acceleration:
import cupy as cp
def coin_change_gpu(amounts, coins):
"""
Process multiple amounts in parallel on GPU
"""
max_amount = cp.max(amounts)
n_amounts = len(amounts)
# Allocate GPU memory
dp = cp.full((n_amounts, max_amount + 1), cp.inf, dtype=cp.float32)
dp[:, 0] = 0
# DP on GPU
for coin in coins:
for i in range(coin, max_amount + 1):
dp[:, i] = cp.minimum(dp[:, i], dp[:, i - coin] + 1)
# Extract results
results = cp.array([dp[idx, amt] for idx, amt in enumerate(amounts)])
return cp.asnumpy(results)
Distributed Processing (Spark):
from pyspark import SparkContext
def process_batch(amounts, coins):
sc = SparkContext()
def solve_single(amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for i in range(1, amount + 1):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount]
amounts_rdd = sc.parallelize(amounts)
results = amounts_rdd.map(solve_single).collect()
return results
28. Memory Optimization Techniques
1. Sparse DP (for large amounts):
def coin_change_sparse(coins, amount):
# Only store reachable states
dp = {0: 0}
for i in range(1, amount + 1):
candidates = []
for coin in coins:
if i - coin in dp:
candidates.append(dp[i - coin] + 1)
if candidates:
dp[i] = min(candidates)
return dp.get(amount, -1)
2. Sliding Window (for streaming amounts):
def coin_change_streaming(coins, max_window=1000):
"""
Process amounts in a stream without storing full DP table
"""
dp = [float('inf')] * max_window
dp[0] = 0
for i in range(1, max_window):
for coin in coins:
if i >= coin:
dp[i] = min(dp[i], dp[i - coin] + 1)
def query(amount):
if amount < max_window:
return dp[amount]
else:
# Compute on-demand for large amounts
return coin_change_large(coins, amount)
return query
29. Ethical Considerations
1. Cashless Society:
- Optimizing coin change is less relevant as we move to digital payments.
- Impact: Marginalizes unbanked populations who rely on cash.
- Policy: Laws requiring businesses to accept cash (e.g., in NYC).
2. Algorithmic Pricing:
- Dynamic pricing (Uber surge) is a form of resource allocation.
- Risk: Price gouging during emergencies.
- Regulation: Caps on surge pricing during disasters.
30. Further Reading
- “The Art of Computer Programming, Vol 3” (Knuth): Generating functions.
- “Algorithms” (Dasgupta): DP chapter.
- “Coin Problem” (MathWorld): Frobenius numbers.
- “High-Frequency Trading” (Aldridge): Arbitrage strategies.
31. Conclusion
The Coin Change problem is a masterclass in Dynamic Programming. It teaches us about state transition, the importance of loop order (permutations vs. combinations), and the dangers of greedy algorithms. From making change at a bodega to detecting arbitrage opportunities in global FX markets, the principles of “optimizing a sum of parts” are universal. Whether you solve it with a simple 1D array or a complex generating function, mastering Coin Change is a rite of passage for every computer scientist.
32. Summary
| Approach | Time | Space | Notes |
|---|---|---|---|
| DP | $O(N \cdot A)$ | $O(A)$ | Standard solution |
| BFS | $O(N \cdot A)$ | $O(A)$ | Graph perspective |
| Greedy | $O(N)$ | $O(1)$ | Only for canonical systems |
Where $N$ = number of coin types, $A$ = amount.
Originally published at: arunbaby.com/dsa/0038-coin-change