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.


Understanding the Coin Change Problem



Coin Change asks us to find the minimum number of coins needed to make up a given target amount, using coins of specified denominations. If it’s impossible to achieve the target amount with the given denominations, the function should return -1.

Inputs and Outputs

Input:

1. An array of integers representing the available coin denominations (e.g., [1, 2, 5]).

2. An integer representing the target amount (e.g., 11).

Output:

• The minimum number of coins required to reach the target amount.

• If it’s not possible to reach the target amount with the given denominations, return -1.



Solving the Coin Change Problem Using Brute Force


The brute-force approach is the simplest way to solve the problem. It works by recursively trying all possible combinations of coins to reach the target amount.

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;
}
}


When solving the Coin Change problem, the heart of the recursive approach lies in systematically exploring all possible ways to reach the target amount by subtracting available coin denominations. This piece of code encapsulates that logic:

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 *