DP 20. Minimum Coins | DP on Subsequences | Infinite Supplies Pattern
This chapter explores the Minimum Coins problem, a classic variation of Dynamic Programming on Subsequences featuring infinite supplies of coin denominations. We examine why the intuitive Greedy algorithm fails for arbitrary coin systems, build the complete recursive mental model (Pick and Non-Pick with multiple uses), optimize via Memoization and Tabulation, and finally achieve optimal Space Optimization.
1. Problem Statement & The Status Quo
The Core Objective
Given an array of coin denominations (e.g., [1, 2, 3]) and a target sum (e.g., target = 7), find the minimum number of coins required to exactly form the target sum. You have an infinite supply of each coin denomination, meaning any coin can be used zero, one, multiple, or infinitely many times as long as the sum equals the target.
Example Walkthrough
- Denominations:
[1, 2, 3] - Target:
7 - Valid Combinations:
3 + 3 + 1 = 7(Total coins used:3)2 + 2 + 2 + 1 = 7(Total coins used:4)1 + 1 + ... + 1 = 7(Total coins used:7)
- Expected Output:
3(the minimum coin count).
2. Why the Greedy Approach Fails
A natural first instinct is to use a Greedy strategy—always picking the largest possible coin denomination that fits into the remaining target, repeating until the target is met.
Where Greedy Works
If denominations are [1, 2, 3] and target is 7:
- Pick largest coin (Remainder: )
- Pick largest coin (Remainder: )
- Pick largest coin (Remainder: )
- Total Coins: (Optimal!).
Where Greedy Fails (Lack of Uniformity)
Consider denominations [9, 6, 5, 1] and target 11.
- Greedy Execution:
- Largest coin is
9. Pick one9. Remainder = . - Next coins (
6,5) are too large for remainder2. - Pick coin
1twice. Remainder = . - Total Greedy Coins:
3(9 + 1 + 1).
- Largest coin is
- Optimal Execution:
- Pick
6and5. Sum = . - Total Optimal Coins:
2(6 + 5).
- Pick
Why Greedy Fails: Greedy fails because there is no uniformity in the differences between coin denominations. Picking the locally largest coin often traps us in sub-optimal paths where smaller subsequent coins cannot efficiently bridge the remaining target.
3. Core Mental Model: Try All Combinations
Since Greedy is unreliable, we must systematically explore all possible combinations of coin selections. Because this falls under DP on Subsequences, our foundational pattern is Pick and Non-Pick, adapted here for infinite supplies.
Real-Life Analogy: The Cash Register Drawer
Imagine standing in front of a cash register with infinite bills of various denominations (2, 10). To pay an exact bill of 5 bill versus skipping it and trying smaller bills, ensuring every branch is explored.
4. Step-by-Step Recursive Definition & Recurrence
To define our recursive function f(index, target), we establish three core parameters:
- Index: The current coin denomination index we are considering (
n - 1down to0). - Target: The remaining value we need to form.
- Return Value: The minimum number of coins required to form the target using coins from index
0toindex.
The Two Choices (Infinite Supply Twist)
Unlike standard knapsack or subsequence problems where picking an item forces an index decrement (index - 1), infinite supply changes the behavior:
- Non-Pick: We skip the current coin index entirely. We move to
index - 1with the same target. - Pick: We take the coin at the current index (provided
coins[index] <= target). Because we can reuse it, we stay at the same index (index), but we reduce our target bycoins[index], and add1to our coin count.
f(index, target)
/ \
(Skip/Non-Pick) / \ (Take/Pick)
v v
f(index - 1, target) 1 + f(index, target - coins[index])
(Moves to prev coin) (Stays at same coin, target reduced)
Recurrence Relation Formulation
# 1. Non-Pick Case
not_take = 0 + f(index - 1, target)
# 2. Pick Case (Condition: coins[index] <= target)
take = INT_MAX
if coins[index] <= target:
take = 1 + f(index, target - coins[index])
# 3. Combine
return min(not_take, take)
5. Base Cases & Handling Edge Conditions
When our recursion reaches index == 0, we are left with only a single coin denomination at the very beginning of the array.
Analyzing Index 0
Suppose index == 0 and our single coin is coins[0] = 6:
- If
target = 12, we can use two coins:12 / 6 = 2. - If
target = 7,7is not divisible by6, so it's impossible to form the target using only6. We must return a very large number (1e9) to signal an invalid path.
if index == 0:
if target % coins[0] == 0:
return target / coins[0]
else:
return 1e9 # Represents infinity / impossible path
Note on Overflow Safety: We use
1e9instead of standardINT_MAX(2^31 - 1). When our recursive steps add1 + f(...), adding1toINT_MAXcauses integer overflow bugs.1e9is safely large enough to act as infinity without risking overflow.
6. Complete Recursive & Memoized Implementation
Recursive Code Structure
int f(int ind, int T, vector<int>& nums) {
if (ind == 0) {
if (T % nums[0] == 0) return T / nums[0];
return 1e9;
}
int notTake = 0 + f(ind - 1, T, nums);
int take = 1e9;
if (nums[ind] <= T) {
take = 1 + f(ind, T - nums[ind], nums);
}
return min(notTake, take);
}
Memoization Matrix (O(N * Target))
Because subproblems overlap heavily (different paths reaching the same index with the same remaining target), we memoize states using a 2D DP table of size [N][target + 1] initialized to -1`.
int f(int ind, int T, vector<int>& nums, vector<vector<int>>& dp) {
if (ind == 0) {
if (T % nums[0] == 0) return T / nums[0];
return 1e9;
}
if (dp[ind][T] != -1) return dp[ind][T];
int notTake = 0 + f(ind - 1, T, nums, dp);
int take = 1e9;
if (nums[ind] <= T) {
take = 1 + f(ind, T - nums[ind], nums, dp);
}
return dp[ind][T] = min(notTake, take);
}
7. Tabulation (Bottom-Up DP)
To convert memoization into bottom-up tabulation:
- Declare a DP table:
vector<vector<int>> dp(n, vector<int>(target + 1, 0)). - Translate the base case (
ind == 0) for all possible targets from0totarget. - Run nested loops for
ind(1ton-1) andtarget(0totarget). - Copy the recurrence relation directly.
Tabulation Code
int minimumElements(vector<int>& nums, int target) {
int n = nums.size();
vector<vector<int>> dp(n, vector<int>(target + 1, 0));
// Base Case Initialization for index 0
for (int t = 0; t <= target; t++) {
if (t % nums[0] == 0) dp[0][t] = t / nums[0];
else dp[0][t] = 1e9;
}
// Fill table bottom-up
for (int ind = 1; ind < n; ind++) {
for (int t = 0; t <= target; t++) {
int notTake = dp[ind - 1][t];
int take = 1e9;
if (nums[ind] <= t) {
take = 1 + dp[ind][t - nums[ind]]; // Notice staying on 'ind' for infinite supply
}
dp[ind][t] = min(notTake, take);
}
}
int ans = dp[n - 1][target];
if (ans >= 1e9) return -1;
return ans;
}
8. Space Optimization
Notice that computing row ind only requires values from the current row ind (for take) and the immediate previous row ind - 1 (for notTake). We can optimize space from down to using two 1D arrays: prev and curr.
Space-Optimized Implementation
int minimumElements(vector<int>& nums, int target) {
int n = nums.size();
vector<int> prev(target + 1, 0), curr(target + 1, 0);
// Base Case
for (int t = 0; t <= target; t++) {
if (t % nums[0] == 0) prev[t] = t / nums[0];
else prev[t] = 1e9;
}
for (int ind = 1; ind < n; ind++) {
for (int t = 0; t <= target; t++) {
int notTake = prev[t];
int take = 1e9;
if (nums[ind] <= t) {
take = 1 + curr[t - nums[ind]]; // Uses curr because coin can be reused
}
curr[t] = min(notTake, take);
}
prev = curr;
}
int ans = prev[target];
if (ans >= 1e9) return -1;
return ans;
}
9. Complexity Analysis Summary
10. Summary and Key Takeaways
- Subsequence Pattern: This problem extends the standard subsequence framework by introducing infinite supplies of items.
- Why Greedy Fails: Arbitrary coin systems lack uniformity in denomination differences, making greedy choices locally optimal but globally flawed.
- The Take Rule: When an item has infinite supply, do not decrement the index upon taking it; stay at the same index (
ind) and reduce the target (target - coins[ind]). - Safety Constants: Always use a safe infinity sentinel like
1e9instead ofINT_MAXto prevent integer overflow when adding1 + f(...). - Base Case Precision: Base cases at
ind == 0must check divisibility (target % nums[0] == 0) to determine if the target can be formed exclusively using the smallest coin. - Space Reduction: Row dependency is restricted to the previous row (
notTake) and current row (take), enabling space optimization.