diff --git a/DP/#322 - Coin Change - Medium/Explanation.md b/DP/#322 - Coin Change - Medium/Explanation.md new file mode 100644 index 0000000..68774b4 --- /dev/null +++ b/DP/#322 - Coin Change - Medium/Explanation.md @@ -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(2N + 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 diff --git a/DP/#322 - Coin Change - Medium/coinChange.c b/DP/#322 - Coin Change - Medium/coinChange.c new file mode 100644 index 0000000..46133cb --- /dev/null +++ b/DP/#322 - Coin Change - Medium/coinChange.c @@ -0,0 +1,154 @@ +//Plain Recursion +#include +#include + +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 +#include +#include + +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 +#include +#include + +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 +#include + +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 +#include + +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) +*/ \ No newline at end of file diff --git a/DP/#322 - Coin Change - Medium/coinChange.cpp b/DP/#322 - Coin Change - Medium/coinChange.cpp new file mode 100644 index 0000000..aa5950d --- /dev/null +++ b/DP/#322 - Coin Change - Medium/coinChange.cpp @@ -0,0 +1,135 @@ +//Plain Recursion +class Solution { +public: + int recur(int ind, int amount, vector& coins){ + if (ind == 0) { + if (amount % coins[0] == 0) return amount / coins[0]; + else return 1e9; + } + int take = 1e9; + if(amount>=coins[ind]){ + take = recur(ind,amount-coins[ind],coins)+1; + } + int notTake = recur(ind-1,amount,coins); + return min(take,notTake); + } + + int coinChange(vector& coins, int amount) { + int n=coins.size(); + int minCoins = recur(n-1,amount,coins); + return minCoins==1e9 ? -1 : minCoins; + } +}; +//Time Limit Exceeded (TLE) for large inputs + + +//Memoziation +class Solution { +public: + vector> dp; + int recur(int ind, int amount, vector& coins){ + if (ind == 0) { + if (amount % coins[0] == 0) return amount / coins[0]; + else return 1e9; + } + if(dp[ind][amount]!=-1) return dp[ind][amount]; + int take = 1e9; + if(amount>=coins[ind]){ + take = recur(ind,amount-coins[ind],coins)+1; + } + int notTake = recur(ind-1,amount,coins); + dp[ind][amount] = min(take,notTake); + return dp[ind][amount]; + } + + int coinChange(vector& coins, int amount) { + int n=coins.size(); + dp = vector>(n,vector(amount+1,-1)); + for(int i=0;i& coins, int amount) { + int n=coins.size(); + vector> dp(n,vector(amount+1,1e9)); + for(int i=0;i<=amount;i++) if(i%coins[0]==0) dp[0][i]=i/coins[0]; + for(int i=0;i& coins, int amount) { + int n=coins.size(); + vector curr(amount+1,1e9); + vector prev(amount+1,1e9); + 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& coins, int amount) { + int n=coins.size(); + vector dp(amount+1,1e9); + sort(coins.begin(),coins.end()); + dp[0]=0; + for(int i=1;i<=amount;i++){ + for (int c:coins){ + if(c>i) break; + dp[i] = min(dp[i],dp[i-c]+1); + } + } + return dp[amount]==1e9 ? -1 :dp[amount]; + } +}; +/* + Runtime: 23 ms + Memory Usage: 15.69 MB + Time Complexity : O(n*t) +*/ \ No newline at end of file diff --git a/DP/#322 - Coin Change - Medium/coinChange.java b/DP/#322 - Coin Change - Medium/coinChange.java new file mode 100644 index 0000000..6ec61f3 --- /dev/null +++ b/DP/#322 - Coin Change - Medium/coinChange.java @@ -0,0 +1,141 @@ +//Plain Recursion +class Solution { + private int recur(int ind, int amount, int[] coins) { + if (ind == 0) { + if (amount % coins[0] == 0) return amount / coins[0]; + else return (int)1e9; + } + int take = (int)1e9; + if (amount >= coins[ind]) { + take = recur(ind, amount - coins[ind], coins) + 1; + } + int notTake = recur(ind - 1, amount, coins); + return Math.min(take, notTake); + } + + public int coinChange(int[] coins, int amount) { + int n = coins.length; + int minCoins = recur(n - 1, amount, coins); + return minCoins == (int)1e9 ? -1 : minCoins; + } +} +//Time Limit Exceeded (TLE) for large inputs + +//Recursion with Memoization +class Solution { + int[][] dp; + + private int recur(int ind, int amount, int[] coins) { + if (ind == 0) { + return (amount % coins[0] == 0) ? amount / coins[0] : (int)1e9; + } + if (dp[ind][amount] != -1) return dp[ind][amount]; + + int take = (int)1e9; + if (amount >= coins[ind]) { + take = 1 + recur(ind, amount - coins[ind], coins); + } + int notTake = recur(ind - 1, amount, coins); + + return dp[ind][amount] = Math.min(take, notTake); + } + + public int coinChange(int[] coins, int amount) { + int n = coins.length; + dp = new int[n][amount + 1]; + for (int[] row : dp) Arrays.fill(row, -1); + for (int i = 0; i < n; i++) dp[i][0] = 0; + + int minCoins = recur(n - 1, amount, coins); + return minCoins == (int)1e9 ? -1 : minCoins; + } +} +/* + Runtime: 27 ms + Memory Usage: 45.80 MB + Time Complexity : O(n*t) +*/ + +//Tabulation +class Solution { + public int coinChange(int[] coins, int amount) { + int n = coins.length; + int[][] dp = new int[n][amount+1]; + for (int[] row : dp) Arrays.fill(row, (int)1e9); + + 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)1e9; + dp[i][j] = Math.min(take, notTake); + } + } + return dp[n-1][amount] == (int)1e9 ? -1 : dp[n-1][amount]; + } +} + +/* + Runtime: 18 ms + Memory Usage: 45.63 MB + Time Complexity : O(n*t) +*/ + +//Space Optimizzed Tabulation +class Solution { + public int coinChange(int[] coins, int amount) { + int n = coins.length; + int[] prev = new int[amount+1]; + int[] curr = new int[amount+1]; + Arrays.fill(prev, (int)1e9); + Arrays.fill(curr, (int)1e9); + + 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)1e9; + curr[j] = Math.min(take, notTake); + } + prev = curr.clone(); + } + return prev[amount] == (int)1e9 ? -1 : prev[amount]; + } +} +/* + Runtime: 17 ms + Memory Usage: 45.14 MB + Time Complexity : O(n*t) +*/ + +//1D Tabulation +class Solution { + public int coinChange(int[] coins, int amount) { + Arrays.sort(coins); + int[] dp = new int[amount+1]; + Arrays.fill(dp, (int)1e9); + dp[0] = 0; + + for (int i = 1; i <= amount; i++) { + for (int c : coins) { + if (c > i) break; + dp[i] = Math.min(dp[i], dp[i-c] + 1); + } + } + return dp[amount] == (int)1e9 ? -1 : dp[amount]; + } +} +/* + Runtime: 15 ms + Memory Usage: 44.35 MB + Time Complexity : O(n*t) +*/ + + diff --git a/DP/#322 - Coin Change - Medium/coinChange.py b/DP/#322 - Coin Change - Medium/coinChange.py new file mode 100644 index 0000000..fcf98db --- /dev/null +++ b/DP/#322 - Coin Change - Medium/coinChange.py @@ -0,0 +1,118 @@ +#Plain Recursion +class Solution: + def recur(self, ind, amount, coins): + if ind == 0: + return amount // coins[0] if amount % coins[0] == 0 else int(1e9) + take = int(1e9) + if amount >= coins[ind]: + take = 1 + self.recur(ind, amount - coins[ind], coins) + notTake = self.recur(ind - 1, amount, coins) + return min(take, notTake) + + def coinChange(self, coins, amount): + n = len(coins) + minCoins = self.recur(n-1, amount, coins) + return -1 if minCoins == int(1e9) else minCoins +#Time Limit Exceeded (TLE) for large inputs + +#Recursion with Memoization +class Solution: + def coinChange(self, coins, amount): + n = len(coins) + dp = [[-1]*(amount+1) for _ in range(n)] + for i in range(n): + dp[i][0] = 0 + + def recur(ind, amt): + if ind == 0: + return amt // coins[0] if amt % coins[0] == 0 else int(1e9) + if dp[ind][amt] != -1: + return dp[ind][amt] + take = int(1e9) + if coins[ind] <= amt: + take = 1 + recur(ind, amt - coins[ind]) + notTake = recur(ind-1, amt) + dp[ind][amt] = min(take, notTake) + return dp[ind][amt] + + res = recur(n-1, amount) + return -1 if res == int(1e9) else res +''' + Runtime: 965 ms + Memory Usage: 46.32 MB + Time Complexity : O(n*t) +''' + +#Tabulation +class Solution: + def coinChange(self, coins, amount): + n = len(coins) + dp = [[int(1e9)]*(amount+1) for _ in range(n)] + + for i in range(amount+1): + if i % coins[0] == 0: + dp[0][i] = i // coins[0] + for i in range(n): + dp[i][0] = 0 + + for i in range(1, n): + for j in range(1, amount+1): + notTake = dp[i-1][j] + take = 1 + dp[i][j-coins[i]] if coins[i] <= j else int(1e9) + dp[i][j] = min(take, notTake) + + return -1 if dp[n-1][amount] == int(1e9) else dp[n-1][amount] +''' + Runtime: 878 ms + Memory Usage: 19.55 MB + Time Complexity : O(n*t) +''' + +#Space Optimized Tabulation +class Solution: + def coinChange(self, coins, amount): + n = len(coins) + prev = [int(1e9)]*(amount+1) + curr = [int(1e9)]*(amount+1) + + for i in range(amount+1): + if i % coins[0] == 0: + prev[i] = i // coins[0] + prev[0] = 0 + + for i in range(1, n): + curr[0] = 0 + for j in range(1, amount+1): + notTake = prev[j] + take = 1 + curr[j-coins[i]] if coins[i] <= j else int(1e9) + curr[j] = min(take, notTake) + prev = curr[:] + + return -1 if prev[amount] == int(1e9) else prev[amount] +''' + Runtime: 754 ms + Memory Usage: 18.34 MB + Time Complexity : O(n*t) +''' + +#1D Tabulation +class Solution: + def coinChange(self, coins, amount): + coins.sort() + dp = [int(1e9)]*(amount+1) + dp[0] = 0 + + for i in range(1, amount+1): + for c in coins: + if c > i: + break + dp[i] = min(dp[i], dp[i-c]+1) + + return -1 if dp[amount] == int(1e9) else dp[amount] +''' + Runtime: 679 ms + Memory Usage: 17.98 MB + Time Complexity : O(n*t) +''' + +