0

I understand how the greedy algorithm for the coin change problem works. I am just a bit confused on when the output is always optimal (when using the Greedy) and when the greedy will always fail.

For example the US coin denominations are canonical meaning that it is always optimal, what are coin denominations that will always fail or always be optimal?

King
  • 1
  • 1

1 Answers1

3

There is an O(n^3) algorithm derived by Pearson (1994, 2004) for determining whether a given system of n coins is canonical. I believe this is still the best general case algorithm. It works by isolating O(n^2) possible values into which the smallest counterexample to the greedy algorithm must fall (if a counterexample exists). The paper gives a rule for generating a potential counterexample and a minimum coin representation for it in O(n) time. You then check whether the greedy algorithm produces a representation with more coins than the derived minimal one (also in O(n) time), and do this for all O(n^2) potential counterexamples. If the greedy matches the minimal in terms of number of coins for all potential counterexamples, then no counterexample exists and greedy algorithm is optimal for all values (otherwise, it is of course not optimal, as you've found a counterexample).

Here is an implementation of the test:

/**
 * Check if coins can be used greedily to optimally solve
 * the  change-making problem
 *  check all potential minimal counter-examples w_{ij}, 1 < i <= j <= n: 
 *  An optimal coinVector for w_{ij} equals the greedy coinVector 
 *  for c_{i-1}-1 with the coint of c_j incremented by one and all 
 *   following counts set equal to zero
 * coins: [c1, c2, c3...] : sorted descending with cn = 1
 * return: [optimal?, minimalCounterExample | null, greedySubOptimal | null] */
function greedyIsOptimal(coins) {
  for (let i = 1; i < coins.length; i++) {
    greedyVector = makeChangeGreedy(coins, coins[i - 1] - 1)
    for (let j = i; j < coins.length; j++) {
      let [minimalCoins, w_ij] = getMinimalCoins(coins, j, greedyVector)
      let greedyCoins = makeChangeGreedy(coins, w_ij)
      if (coinCount(minimalCoins) < coinCount(greedyCoins))
        return [false, minimalCoins, greedyCoins]
    }
  }
  return [true, null, null]
}

// coins [c1, c2, c3...] => greedy coinVector for amount
function makeChangeGreedy(coins, amount) {
  return coins.map(c => {
    let numCoins = Math.floor(amount / c);
    amount %= c
    return numCoins;
  })
}
// generate a potential counter-example in terms of its coinVector 
// and total amount of change
function getMinimalCoins(coins, j, greedyVector) {
  minimalCoins = greedyVector.slice();
  minimalCoins[j - 1] += 1
  for (let k = j; k < coins.length; k++) minimalCoins[k] = 0
  return [minimalCoins, evaluateCoinVector(coins, minimalCoins)]
}
// return the total amount of change for coinVector
const evaluateCoinVector = (coins, coinVector) =>
  coins.reduce((change, c, i) => change + c * coinVector[i], 0)

// return number of coins in coinVector
const coinCount = (coinVector) =>
  coinVector.reduce((count, a) => count + a, 0)

/* Testing */
let someFailed = false;
function test(coins, expect) {
  console.log(`testing ${coins}`)
  let [optimal, minimal, greedy] = greedyIsOptimal(coins)
  if (optimal != expect) (someFailed = true) && console.error(`expected optimal=${expect}
  optimal: ${optimal}, amt:${evaluateCoinVector(coins, minimal)}, min: ${minimal}, greedy: ${greedy}`)
}
// canonical examples
test([25, 10, 5, 1], true) // USA
test([240, 60, 24, 12, 6, 3, 1], true) // Pound Sterling - 30
test([240, 60, 30, 12, 6, 3, 1], true) // Pound Sterling - 24
test([16, 8, 4, 2, 1], true) // Powers of 2
test([5, 3, 1], true) // Simple case

// non-canonical examples
test([240, 60, 30, 24, 12, 6, 3, 1], false) // Pound Sterling
test([25, 12, 10, 5, 1], false) // USA + 12c
test([25, 10, 1], false) // USA - nickel
test([4, 3, 1], false) // Simple cases
test([6, 5, 1], false)

console.log(someFailed ? "test(s) failed" : "All tests passed.")
inordirection
  • 959
  • 6
  • 16