Coin Change – 322. LeetCode


The “Coin Change” problem is a classic example of a dynamic programming problem in which we aim to find the minimum number of coins needed to make up a target amount using a given set of coin denominations. This problem is known for testing the ability to optimize for minimal resources under constraints, a core feature of many real-world optimization problems.

Brute Force Algorithm Solution


In the brute-force approach, we recursively try every possible combination of coins to see if we can make up the target amount, tracking the minimum number of coins used.

public class CoinChange {
public int coinChange(int[] coins, int amount) {
int result = helper(coins, amount);
return result == Integer.MAX_VALUE ? -1 : result;
}

private int helper(int[] coins, int amount) {
if (amount == 0) return 0;
if (amount < 0) return Integer.MAX_VALUE;

int minCoins = Integer.MAX_VALUE;
for (int coin : coins) {
int res = helper(coins, amount - coin);
if (res != Integer.MAX_VALUE) {
minCoins = Math.min(minCoins, res + 1);
}
}
return minCoins;
}
}


Optimal Algorithm and Data Structure Choice



Using dynamic programming (DP) to solve this problem will improve the efficiency of the code by avoiding redundant calculations. In the current code, the helper function is a recursive brute-force approach, which recalculates results for the same sub-amounts repeatedly, leading to a large number of recursive calls. This approach has an exponential time complexity due to the overlapping subproblems and lack of stored intermediate results.

In dynamic programming, there are two main approaches:

Top-Down (Memoization): Starting from the main problem and breaking it down into subproblems recursively, storing each result.

Bottom-Up (Tabulation): Building up solutions from the smallest subproblems and iteratively combining them to reach the main problem’s solution.


The bottom-up approach will be used to refactor this code, making it both time-efficient and space-efficient.


Steps of the Algorithm

1. Create a dp array with size amount + 1 initialized to a high value, with dp[0] = 0 (0 coins to make up amount 0).

2. For each coin, iterate through the dp array from the coin’s value up to the target amount.

3. For each index i, update dp[i] to be the minimum of dp[i] and dp[i – coin] + 1.

4. If dp[amount] is still a high value after filling the array, return -1 (target amount is not achievable), else return dp[amount].

public class CoinChange {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount + 1];
Arrays.fill(dp, amount + 1); // Initialize to a value greater than the maximum possible coins
dp[0] = 0; // Base case: 0 coins to make amount 0

for (int coin : coins) {
for (int i = coin; i <= amount; i++) {
dp[i] = Math.min(dp[i], dp[i - coin] + 1); // Update dp[i] if using this coin reduces the coin count
}
}

return dp[amount] > amount ? -1 : dp[amount]; // If dp[amount] is unchanged, return -1
}
}

Line-By-Line Detailed Explanation

  1. Create DP Array
        int[] dp = new int[amount + 1];
  • This line creates an integer array dp of size amount + 1. The array is used to store the minimum number of coins required to reach every amount from 0 up to amount.

2. Initialize Array With Amount + 1

        Arrays.fill(dp, amount + 1);

This line initializes each entry in the dp array to a value of amount + 1, which is greater than any possible number of coins needed.

• By setting each value initially to amount + 1, we ensure that any unreachable amount remains “infinite” in the context of our logic, so that we know if it’s possible to reach amount.

3. Set Our Base Case In Array

        dp[0] = 0;

This line sets dp[0] to 0, establishing our base case.

• To make the amount 0, we need 0 coins, so dp[0] is 0. This serves as the starting point for our dynamic programming logic.

4. Iterate Over Each Coin

    for (int coin : coins) {

This line starts a loop that iterates over each coin denomination in the coins array.

• For each coin, we will update the dp array to calculate the minimum number of coins required for every possible amount from coin up to amount.

5. Iterate To Target Amount

            for (int i = coin; i <= amount; i++) {
  • This nested loop iterates through amounts starting from the current coin’s value up to the target amount.

• i represents the amount we’re trying to make with the coins. Starting from coin ensures that we’re only considering amounts that can be constructed by the given coin.

6. Calculate The Minimum Number of Coins

                dp[i] = Math.min(dp[i], dp[i - coin] + 1);
  • This line calculates the minimum number of coins required for the current amount i.

• dp[i – coin] gives the minimum coins needed to reach the amount (i – coin).

• By adding 1 to dp[i – coin], we count the current coin towards reaching the total amount i.

• Math.min(dp[i], dp[i – coin] + 1) ensures that dp[i] gets the smaller value between its current value and the value if we include this coin. This update step is what ensures that dp[i] always holds the minimum coins needed for each amount i.

7. Return Result

return dp[amount] > amount ? -1 : dp[amount];

This line returns the final result.

• If dp[amount] is greater than amount, it means no combination of coins could sum to amount, so we return -1.

• Otherwise, dp[amount] contains the minimum number of coins required to make up the target amount.

Why This Code Works


This dynamic programming solution avoids redundant recalculations by storing intermediate results in the dp array, while the brute-force approach recalculates values repeatedly, leading to inefficiency. In contrast, dynamic programming reduces both time and space complexity significantly by using memoization (the dp array), making this approach ideal for problems with overlapping subproblems and optimal substructure.

Leave a Reply

Your email address will not be published. Required fields are marked *