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.

13
  • 3
    Did you verify that your original code actually solved the problem? Commented Jun 30, 2023 at 12:31
  • 3
    But making code that produces the wrong answer faster isn't the best approach. Commented Jun 30, 2023 at 12:44
  • 1
    Try indexing your memo on index AND amount. Commented Jun 30, 2023 at 13:53
  • 1
    That global count is problematic. This is not how it's done. Commented Jun 30, 2023 at 17:35
  • 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. Commented Jun 30, 2023 at 18:09

2 Answers 2

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];
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for the elaborate answer, with appropriate commentary. Best wishes to You, Ukraine and the Whole World!!
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?
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.
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. :)
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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.