algorithmrecursiondynamic-programmingmemoizationrecursive-backtracking

How to convert the following backtracking code into a top down head recursive memoized version?


I am trying to solve the famous Coin Change problem, below is the problem statement!!

Coin Change II

You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money.

Return the number of combinations that make up that amount. If that amount of money cannot be made up by any combination of the coins, return 0.

You may assume that you have an infinite number of each kind of coin.

The answer is guaranteed to fit into a signed 32-bit integer.

Below are some examples to illustrate the problem.

Example 1:

Input: amount = 5, coins = [1,2,5]
Output: 4
Explanation: there are four ways to make up the amount:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

Example 2:

Input: amount = 3, coins = [2]
Output: 0
Explanation: the amount of 3 cannot be made up just with coins of 2.

Example 3:

Input: amount = 10, coins = [10]
Output: 1

Constraints:

  • 1 <= coins.length <= 300
  • 1 <= coins[i] <= 5000
  • All the values of coins are unique.
  • 0 <= amount <= 5000

On seeing this problem :-

My first line of attack was to quickly utilize the fact that at either step of coin selection for the given amount, either we can select the coin or not select it, that is skip it and move ahead to the next coin in the input coin array.

class Solution {
    private int count = 0;

    public int change( int amount, int[] coins) {
        Integer[] arr = Arrays.stream( coins)
                                .boxed()
                                .sorted( Comparator.reverseOrder())
                                .toArray( Integer[]::new);
        backtrack( arr, 0, amount);
        return count;
    }

    private void backtrack( Integer[] coins, int index, int amount) {
        if( index >= coins.length) {
            if( amount == 0) {
                ++count;
            }
        }
        else { // At every coin we have only two choices, 
               // either we take it or we skip it.
            backtrack( coins, index + 1, amount); 
               // This is the skip case, we are incrementing the index
               // by 1. And not taking that coin value into account
               // by not decrementing the current coin value from amount.
            if( coins[index] <= amount)
              backtrack( coins, index, amount - coins[index]);
               // We are taking that coin and subtracting the coin from the
               // amount, not incrementing the index as the same coin can be
               // considered multiple times, so as to be able to consider
               // it multiple times we are not incrementing the index.
        }
    }
}

Even for a naive like me, it feels that my backtracking formulation for the above problem is correct. As I have carefully considered all the possible choices at a given coin selection step, either you take it, and decrement the amount, or skip it, when skipping you increment the index. Backtracking approach is giving TLE(Time Limit Exceeded), as is obvious as there are multiple recursive calls, and hence it is going to take exponential time, but if we cut down on the recursion, we can fix this TLE(Time Limit Exceeded) But when I am trying to convert this backtracking into top down memoized version, I am failing miserably.

Here is my attempt at that :-

class Solution {
    private int count = 0;

    public int change(int amount, int[] coins) {
        int[] memo = new int[amount + 1];
        Arrays.fill( memo, -1);
        backtrack( coins, 0, memo, amount);
        System.out.println( "The content of the array is "
                            + Arrays.toString(memo));
        return memo[amount];
    }

    private void backtrack( int[] coins, int index, int[] memo, int amount) {
        if (index >= coins.length) {
            if (amount == 0) {
                ++count;
                return;
            }
        } else if (memo[amount] != -1){
            return;
        } else {
            backtrack( coins, index + 1, memo, amount);
            if (amount <= coins[index])
              backtrack( coins, index + 1, memo, amount - coins[index]);
            System.out.println( "The value of the count is ---> " 
                    + count 
                    + " and the value of the index is " + index);
            System.out.println( "The content of the array in backtrack is " 
                    + Arrays.toString( memo));  
            memo[amount] += count;  
        }
    }
}

What I intend from this code that it should fill the memo array with the corresponding count value, I want the count to be a global variable and the backtracking method should be returning void, as its return type.

With this template can I be able to fix my code to return the correct value for the amount? So I basically I want to know with minimum changes in the overall program layout can I fix this issue and get the correct result?

I only need to understand that once I have the backtracking approach with me, how to proceed from there to the memoized top down one and then to the bottom up approach, eventually.


Solution

  • The approach will not work, for several reasons, including:

    With recursion you should attempt to solve a smaller -- isolated problem. So a global count is out of the question. Given an index and an amount, the recursive call should solve the problem "how many possibilities exist to consume that amount with the coins available from this index onwards". This count should be the return value of the recursive function.

    Memoization should distinguish between the unique states of the subproblems. In this case the recursive call gets two constraints: the still available coins (index) and the (remaining) amount to cover for. So your memo needs two dimensions, not one.

    Here is the adapted code:

    class Solution {
        public int change(int amount, int[] coins) {
            int[][] memo = new int[amount + 1][coins.length]; // Two dimensions
            for (var row : memo) {
                Arrays.fill(row, -1);
            }
            return backtrack(coins, 0, memo, amount);
        }
    
        private int backtrack(int[] coins, int index, int[][] memo, int amount){
            if (amount == 0) { // At any time we're interested in a match
                return 1; // Found a solution. Callers will add this to their own counts
            }
            if (index >= coins.length || amount < 0) {
                return 0; // No further matches possible without backtracking
            }
            if (memo[amount][index] == -1) {
                // Don't take this coin or take it one or more times:
                memo[amount][index] = backtrack(coins, index + 1, memo, amount)
                                    + backtrack(coins, index, memo, amount - coins[index]);
            }
            return memo[amount][index];
        }
    }