Skip to content

Solution #494 - Coin Change - 3/08/2025 #81

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 158 additions & 0 deletions DP/#322 - Coin Change - Medium/Explanation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# 494. Target Sum

**Difficulty:** Medium
**Category:** Arrays, Dynamic Programming, Recursion
**Leetcode Link:** [Problem Link](https://leetcode.com/problems/coin-change)

---

## 📝 Introduction

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 that you need to make up that amount. If that amount of money 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.

---

## 💡 Approach & Key Insights

The first approach that may come to mind is the greedy approach where the largest coin less than the amount is picked and it is then subtracted from the amount and repeated until it reaches zero. This approach assujes that the local optimal choices lead to a global optimal, however this is not always the case. Sometimes, a set of smaller coins may require fewer number of coins as compared to picking coins of higher denomination. This is better explained by the below example:

`Coin denominations: [1,6,9]`
`Target amount = 12`
- Greedy : 9 + 1 + 1 + 1 = 4 coins
- Optimal : 6 + 6 : 2 coins


This problem can instead be solved by generating all possible combinations using recursion. However, recursion without memoization can severely impact the time complexity of the problem and can exceed the time limit for most of the cases.

---

## 🛠️ Breakdown of Approaches

### 1️⃣ Plain Recursion

- **Explanation:**
For every coin in the array, there are two choices:
- either pick the coin, which will reduce the amount by the coin's denomination. Since the coin can be picked multiple times, we will continue this process until the amount becomes smaller than the coin's denomination.
- or don't pick the coin, in this case the amount remains the same but we move on to the next coin without considering the current coin.
The minimum possible coin would be minimum of `pick + 1` and `notPick`. There are certain base cases to be noted, if amount is 0, then 0 is returned, and if amount is not attainable, infinity (or a very large number) is returned.


- **Time Complexity:** O(2^N * T)
Where N is the number of coins and T is the target amount. For each value of amount (from 0 to T) two possibilities, pick and not pick (2^N for successive recursive calls), can be explored for every coin denomination.

- **Space Complexity:** O(N)
For the auxiliary stack space.


---

### 2️⃣ Recursion with memoization

- **Explanation:**
The above solutions takes an especially long time due to repeated sub problems. Instead of redoing these repeated problems, their return value can be stored so that it may be quickly accessed a second time. The return values can be stored in a memoization table in the form of a 2D array, with the rows representing the coin denominations and the columns representing amount from `0` to `amount`. All the values in the memoization table is initialized with infinity (or a very large value). The rest of the function works in a manner similar to the plain recursion method except for some slight modifications, at the start of the recursive function, if the corresponding value in the memoization table is not infinity, then that value is returned, else the function continues normally and updates the value in the memoization table with the obtained result. This will remove unnecessary recursive calls for repeated sub-problems.

- **Time Complexity:** O(N * T)
Where `N` is the number of coins and `T` is the amount to be obtained, since ideally the program is supposed to operate over each combination of number of denominations and remaining amount.

- **Space Complexity:** O(N * T) + O(N)
For storing 2D DP memoization and for the auxiliary stack space.


---

### 3️⃣ Tabulation

- **Explanation:**
In order to remove the auxiliary stack space and the unecessary backtracking, we can use a top-down approach using tabulation. The first column of the 2D array is enumerated with `0`'s and the first row filled with `amount/coins[0]` where amount is divisible by coins[0] valid otherwise it is filled with infinity. The rest of the cells can be filled with the minimum vakue between `dp[i][j-coins[i]] + 1` and `dp[i-1][j]` where dp is the 2D tabulation, i is the row of the cell and j is the column of the cell.

- **Time Complexity:** O(N * T)
Since the program iterates over all amounts from 0 to T (representing the number of rows in the table) for each denomination of coin (up to N coins).

- **Space Complexity:** O(N * T)
For storing the table.

---

### 4️⃣ Space Optimized Tabulation

- **Explanation:**
Since at a time only two rows are utilized for the tabulation code, we can use just two rows for the storage of the tabulation data, First initialize a `prev` array to store the initial values for 1 coin, and a `curr` array to be filled. After `curr` is filled, it is swapped with `prev`. The final value is stored at the end of the `prev` array after iterating for all the coins.

- **Time Complexity:** O(N * T)
For iterating over N coins for T amount.

- **Space Complexity:** O(T)
For using a 1D array to store values.


---

### 5️⃣ 1D Tabulation

- **Explanation:**
The problem can be solved by using a single 1D tabulation array where `dp[i]` stores the minimum number of coins to attain `i` amount. For each `i` in `dp`, `dp[i]` is the minimum `dp[i-1]` and `dp[i-coins[j]+1` for `j` represent the index of the coin in `coins` array for all coins in the array. The last element of the array holds the final answer.

- **Time Complexity:** O(N * T)
To iterate over all coins for all amounts in the given range.

- **Space Complexity:** O(T)
To store 1D tabulation.


---

## 📊 Complexity Analysis

| Approach | Time Complexity | Space Complexity |
| ----------------------- | ------------------------- | ---------------------- |
| Plain Recursion | O(2<sup>N</sup> + T) | O(N) |
| Memoization | O(N * T) | O(N * T) + O(N) |
| Tabulation | O(N * T) | O(N * T) |
| Optimized Tabulation | O(N * T) | O(T) |
| 1D Tabulation | O(N * T) | O(T) |

---

## 📉 Optimization Ideas

- Go for a top-down approach instead of a top dowon one.
- Prefer 1D tabulation over 2D table.

---

## 📌 Example Walkthroughs & Dry Runs

plaintext
Example:
Input: coins = [1,2,5], amount = 11
Iterating for all elements of the dp array where dp[i] repressents minimum number of coins to attain i amount
- dp = [0,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,2,-1,-1,-1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,2,2,-1,-1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,1,2,2,-1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,1,2,2,1,-1,-1,-1,-1,-1,-1]
- dp = [0,1,1,2,2,1,2,-1,-1,-1,-1,-1]
- dp = [0,1,1,2,2,1,2,2,-1,-1,-1,-1]
- dp = [0,1,1,2,2,1,2,2,3,-1,-1,-1]
- dp = [0,1,1,2,2,1,2,2,3,3,-1,-1]
- dp = [0,1,1,2,2,1,2,2,3,3,2,-1]
- dp = [0,1,1,2,2,1,2,2,3,3,2,3]


Output: 3

---

## 🔗 Additional Resources

- [GeeksForGeeks Explanation](https://www.geeksforgeeks.org/dsa/coin-change-dp-7/)

---

Author: Vatsal Ojha
Date: 3/08/2025
154 changes: 154 additions & 0 deletions DP/#322 - Coin Change - Medium/coinChange.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
//Plain Recursion
#include <stdio.h>
#include <limits.h>

int recur(int ind, int amount, int coins[]) {
if (ind == 0) {
if (amount % coins[0] == 0) return amount / coins[0];
else return INT_MAX/2; // to avoid overflow
}
int take = INT_MAX/2;
if (amount >= coins[ind]) {
take = recur(ind, amount - coins[ind], coins) + 1;
}
int notTake = recur(ind - 1, amount, coins);
return take < notTake ? take : notTake;
}

int coinChange(int coins[], int n, int amount) {
int minCoins = recur(n - 1, amount, coins);
return minCoins >= INT_MAX/2 ? -1 : minCoins;
}
//Time Limit Exceeded (TLE) for large inputs

//Recursion with Memoization
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int **dp;

int recurMemo(int ind, int amount, int coins[]) {
if (ind == 0) {
return (amount % coins[0] == 0) ? amount / coins[0] : INT_MAX/2;
}
if (dp[ind][amount] != -1) return dp[ind][amount];

int take = INT_MAX/2;
if (amount >= coins[ind])
take = 1 + recurMemo(ind, amount - coins[ind], coins);

int notTake = recurMemo(ind-1, amount, coins);
dp[ind][amount] = take < notTake ? take : notTake;
return dp[ind][amount];
}

int coinChange(int coins[], int n, int amount) {
dp = malloc(n * sizeof(int*));
for (int i = 0; i < n; i++) {
dp[i] = malloc((amount+1)*sizeof(int));
for (int j = 0; j <= amount; j++) dp[i][j] = -1;
dp[i][0] = 0;
}
int minCoins = recurMemo(n-1, amount, coins);
for (int i = 0; i < n; i++) free(dp[i]);
free(dp);
return minCoins >= INT_MAX/2 ? -1 : minCoins;
}
/*
Runtime: 56 ms
Memory Usage: 39.30 MB
Time Complexity : O(n*t)
*/

//Tabulation
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

int min(int a, int b) { return a < b ? a : b; }

int coinChange(int coins[], int n, int amount) {
int **dp = malloc(n * sizeof(int*));
for (int i = 0; i < n; i++) {
dp[i] = malloc((amount+1)*sizeof(int));
for (int j = 0; j <= amount; j++) dp[i][j] = INT_MAX/2;
}

for (int i = 0; i <= amount; i++)
if (i % coins[0] == 0) dp[0][i] = i / coins[0];
for (int i = 0; i < n; i++) dp[i][0] = 0;

for (int i = 1; i < n; i++) {
for (int j = 1; j <= amount; j++) {
int notTake = dp[i-1][j];
int take = (coins[i] <= j) ? 1 + dp[i][j-coins[i]] : INT_MAX/2;
dp[i][j] = min(take, notTake);
}
}

int ans = dp[n-1][amount] >= INT_MAX/2 ? -1 : dp[n-1][amount];
for (int i = 0; i < n; i++) free(dp[i]);
free(dp);
return ans;
}
/*
Runtime: 55 ms
Memory Usage: 38.93 MB
Time Complexity : O(n*t)
*/

//Space Optimized Tabulation
#include <stdio.h>
#include <limits.h>

int min(int a, int b) { return a < b ? a : b; }

int coinChange(int coins[], int n, int amount) {
int prev[amount+1], curr[amount+1];
for (int i = 0; i <= amount; i++) prev[i] = INT_MAX/2;
for (int i = 0; i <= amount; i++)
if (i % coins[0] == 0) prev[i] = i / coins[0];
prev[0] = 0;

for (int i = 1; i < n; i++) {
curr[0] = 0;
for (int j = 1; j <= amount; j++) {
int notTake = prev[j];
int take = (coins[i] <= j) ? 1 + curr[j-coins[i]] : INT_MAX/2;
curr[j] = min(take, notTake);
}
for (int k = 0; k <= amount; k++) prev[k] = curr[k];
}
return prev[amount] >= INT_MAX/2 ? -1 : prev[amount];
}
/*
Runtime: 32 ms
Memory Usage: 8.04 MB
Time Complexity : O(n*t)
*/

//1D Tabulation
#include <stdio.h>
#include <limits.h>

int min(int a, int b) { return a < b ? a : b; }

int coinChange(int coins[], int n, int amount) {
int dp[amount+1];
for(int i=0;i<=amount;i++) dp[i] = INT_MAX/2;
dp[0] = 0;

for (int i = 1; i <= amount; i++) {
for (int c = 0; c < n; c++) {
if (coins[c] > i) continue;
dp[i] = min(dp[i], dp[i - coins[c]] + 1);
}
}
return dp[amount] >= INT_MAX/2 ? -1 : dp[amount];
}
/*
Runtime: 28 ms
Memory Usage: 8.16 MB
Time Complexity : O(n*t)
*/
Loading