0

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.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
John Doe
  • 2,752
  • 5
  • 40
  • 58
  • 3
    Did you verify that your original code actually solved the problem? – Scott Hunter Jun 30 '23 at 12:31
  • It's giving TLE, as was expected from it, as the problem is basically formulated so that we are coming up with a DP solution either the memoized top down one or the tabular bottom up approach!! I want to fix my backtracking approach by memoizing it first!! After that I will try to convert it to bottom up approach!! – John Doe Jun 30 '23 at 12:40
  • 3
    But making code that produces the wrong answer faster isn't the best approach. – Scott Hunter Jun 30 '23 at 12:44
  • No for sure, I am very much sure that the backtracking approach is not wrong, as there are multiple recursive calls, hence it's taking exponential time, and we are getting "Time Limit Exceeded". So now I have to optimise my code, and that can be done by cutting on the number of recursive calls, by having a memo which stores the result for the frequently occurring calls, we can save on recursion. – John Doe Jun 30 '23 at 12:50
  • You verified it works on the examples you gave? They should be small enough to complete. (A particular implementation of an "approach", however right that approach may be, can still be wrong. You have heard of bugs, no? And until it has been tested, your approach is still in question.) – Scott Hunter Jun 30 '23 at 12:53
  • Absolutely, do you think I am posting questions here without doing the preliminary checks, all prima facie checks which could have been done have been done. – John Doe Jun 30 '23 at 12:56
  • @ScottHunter Please don't downvote, the backtracking approach is not wrong, it is just under optimized. – John Doe Jun 30 '23 at 13:14
  • 1
    Try indexing your memo on index AND amount. – Scott Hunter Jun 30 '23 at 13:53
  • see also [tag:coin-change]. – Will Ness Jun 30 '23 at 17:29
  • 1
    That global count is problematic. This is not how it's done. – trincot Jun 30 '23 at 17:35
  • @trincot You have answered my earlier questions also, very successfully, please please kindly elaborate further. I am still grappling in the dark. If you elaborate the problems or flaws in the above approach, I may accept that as well. I have seen the solutions, 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. – John Doe Jun 30 '23 at 17:59
  • 1
    The count will accumulate, but will survive also through backtracking and recurring into alternative combinations, where that count does not represent anything realistic, yet it is used to store in `memo`. This is just one of the problems, but since you ask to keep your template (with that global counter), I see no good solution. – trincot Jun 30 '23 at 18:09
  • 1
    @trincot Feel free to come up with your approach to the solution, when I asked my understanding was not accurate. I want to understand what will work so that I get correct memoized solution from the backtracking approach, feel free to do the changes!! – John Doe Jun 30 '23 at 18:17

2 Answers2

2

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

  • count is global, and although it accumulates matches correctly at first, it will cause wrong solutions once backtracking has occurred and alternative paths are visited: in those paths, the value of count -- having results from other paths that split off at an earlier index -- is not correct.

  • When memo[amount] is found to be different from -1, no further recursion occurs: this is good, but it is not good that these possibilities are not counted on top of what you already had: if there are two different ways to reach a partial amount, then the number of ways to get from there to the final goal should be counted twice -- once for each alternative.

  • When memo[amount] gets its first count added to it, it was -1, so one count is lost.

  • memo is never used to help define other memo entries, yet this is one of the goals of memoization. In your code memo serves like a visited flag, but memoization should be used to retrieve a result that is then used to resolve another subproblem.

  • The second recursive call gets index + 1, but it should get index, because a coin could be used more than once.

  • if(amount <= coins[index]) is the wrong condition. This should be >=.

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];
    }
}
trincot
  • 317,000
  • 35
  • 244
  • 286
  • 1
    Thanks for the elaborate answer, with appropriate commentary. Best wishes to You, Ukraine and the Whole World!! – John Doe Jun 30 '23 at 21:35
  • The helper method, even though is named as backtracking it is not particularly backtracking, where we choose some option and call the function with the newly choosen option, and after the call is over we unchoose the option. This helper method is rather recursion with memoization for optimization. Am I correct in my understanding @Trincot? – John Doe Jul 02 '23 at 07:19
  • [Backtracking](https://en.wikipedia.org/wiki/Backtracking) is a broad concept and does not refer only to taking a step "back". Recursion is part of this concept. The part where the helper function returns 0 is what the Wikipedia article calls *"abandons a candidate"*... etc. Memoization is indeed the extra that is added to it. – trincot Jul 02 '23 at 07:29
  • 1
    It is actually possible, after restructuring the OP code a little bit, while *preserving* the original approach. I've posted an answer that shows how. :) – Will Ness Jul 04 '23 at 23:40
1

You can try speeding your first version up a bit by turning it into a tail-recursive code, and turning that into a proper loop. The first, non-tail recursive call will stay recursive of course.

Also, we change the control flow a little, stopping right away when the target amount is reached:

private void backtrack( Integer[] coins, 
                        int index, int amount) {
    if( amount == 0) {
        ++count;
    }
    else {
        while( index < coins.length) {

            if( coins[index] <= amount)
              backtrack( coins, index, amount - coins[index]);

            // we're back!
            ++index;   // "tail call"
        }
    }
}

This technique of turning a double-recursive function into recursion+loop can also be seen in this other recent answer of mine (it's about quicksort).

To memoize this, we just need to record the count before and after the recursive call to backtrack, and the difference will be the number of ways this specific index, amount combination contributes to the overall result. So we store it under that pair used as a key in a lookup table of your choice, and if we ever hit this combination again we can just increment the count by that difference right away without doing any more iterations.

With some pseudocode mixed-in,

// static look-up table reset 
//  on each `change` call appropriately
lookup ....;

private void backtrack( Integer[] coins,
                        int index, int amount) {
    if( amount == 0) {
        ++count;
    }
    else if ( (index,amount) is present in lookup ) {
        count += lookup( index, amount);
    }
    else {
        while( index < coins.length) {

            if( coins[index] <= amount){
              int before = count;

              backtrack( coins, index, amount - coins[index]);

              // we're back!
              store( lookup(index, amount - coins[index]),
                     count - before );
            }

            ++index;   // "tail call"
        }
    }
}

Testing in Scheme (Racket, actually), memoization speeds up the test case coins=(1 2 3 5 10 25 50 100) amount=200 ==> 837531 considerably, turning a 3 sec wall clock run into an instantaneous one. For reference, the tests from the question pass, and (1 5 10 25 50) 100 returns 292, as it should.

The Racket code uses hash table for the lookup structure. Thus the lookup table is built top-down here, and might well have holes in it.

Tail call optimization is guaranteed in Racket, so we don't need to do that trick as we did above, converting the recursive code into a loop manually. Here's the code:

#lang racket

;; base-line:
(define (change-ways-1 denoms sum)
  (define (count-ways denoms sum)
    (cond
      ((= 0 sum) 1)
      ((< sum 0) 0)
      ((null? denoms) 0)
      (else (+ (count-ways denoms (- sum (car denoms)))
               ;; we're back !
               (count-ways (cdr denoms) sum)))))
    
  (set! denoms (sort denoms >=))
  (count-ways denoms sum))

;; mimicking the Java code in question:
(define (change-ways-2 denoms sum)
  (define count 0)
  (define (count-ways denoms sum)
    (cond
      ((= 0 sum) (set! count (+ 1 count)))
      ((null? denoms) 0)
      (else (if (>= sum (car denoms))
              (count-ways denoms (- sum (car denoms)))
              0)
            ;; we're back!
            (count-ways (cdr denoms) sum))))
    
  (set! denoms (sort denoms >=))
  (count-ways denoms sum)
  count)

;; with memoization
(define (change-ways denoms sum)
  (define count 0)
  (define hash (make-hash))
  
  (define (count-ways i denoms sum)
    (cond
      ((= 0 sum) (set! count (+ 1 count)))
      ((hash-has-key? hash (cons i sum))
         (set! count (+ count
                        (hash-ref hash (cons i sum)))))
      ((null? denoms) 0)
      (else (if (>= sum (car denoms))
              (let ([before count])
                 (count-ways i denoms (- sum (car denoms)))
                 ;; we're back!
                 (hash-set! hash
                            (cons i (- sum (car denoms)))
                            (- count before)))
              0)
            (count-ways (- i 1) (cdr denoms) sum))))
  
  (set! denoms (sort denoms >=))
  (count-ways (length denoms) denoms sum)
  count)

;;;;
(change-ways '(1 2 5) 5)   ; 4
(change-ways '(2) 3)       ; 0
(change-ways '(10) 10)     ; 1

(change-ways '(1 5 10 25 50) 100)   ; 292
(change-ways '(1 2 5 10 25 50) 100)   ; 3953
(change-ways '(1 2 5 10 25 50 100) 200)   ; 61984
(change-ways '(1 2 3 5 10 25 50 100) 200)   ; 837531
Will Ness
  • 70,110
  • 9
  • 98
  • 181