为啥贪心硬币找零算法对某些硬币组不起作用?

Posted

技术标签:

【中文标题】为啥贪心硬币找零算法对某些硬币组不起作用?【英文标题】:Why does the greedy coin change algorithm not work for some coin sets?为什么贪心硬币找零算法对某些硬币组不起作用? 【发布时间】:2012-11-13 12:35:01 【问题描述】:

我了解硬币找零问题的贪心算法(用尽可能少的硬币支付特定金额)的工作原理 - 它总是选择面额最大且不超过剩余总和的硬币 - 并且它总能找到特定硬币组的正确解决方案。

但是对于某些硬币组,贪心算法会失败。例如,对于集合1, 15, 25 和总和 30,贪心算法首先选择 25,余数为 5,然后是 5 个 1,总共有 6 个硬币。但是硬币数量最少的解决方案是两次选择 15。

一组硬币必须满足什么条件才能使贪心算法找到所有和的最小解?

【问题讨论】:

答案很大程度上取决于算法的作用:硬币很容易变得贪婪,你应该告诉我们算法是做什么的,以及它是如何做到的。 ...算法应该解决的实际问题是什么? 好吧,我想我没问对。如果算法不起作用,那么一组面额必须为真。前任。从 (25, 15, 1) 贪心赚 30 美分给我们 25,1,1,1,1,1 但 15 15 更好。 25 15 和 1 怎么样让贪心不起作用? 这是一个很好的问题,不知道为什么它被否决了。他/她想要解释一组硬币必须满足的约束条件,以便贪心算法(它总是选择最大的硬币)来选择最小数量的硬币来支付任何指定的金额。 @j_random_hacker 这可以从 OP 的评论中推断出来,但不能从问题中推断出来。这个问题本身并没有暗示算法应该做什么,因此这不是一个好问题。 【参考方案1】:

形成拟阵 (https://en.wikipedia.org/wiki/Matroid) 的集合可用于通过贪心方法解决硬币兑换问题。简而言之,拟阵是有序对 M = (S,l) 满足以下条件:

    S 是有限非空集 l 是 S 的一个非空子集族,称为独立子集,如果 B->l 并且 A 是 B 的子集,则 A -> l 如果 A-> l、B-> l 和 |A| B-A 使得 A U x ->l

在我们的硬币兑换问题中,S 是所有硬币按价值降序排列的集合 我们需要通过 S 中的最少硬币数来达到 V 的值

在我们的例子中,l 是一个包含所有子集的独立集合,因此以下对于每个子集都成立:它们中的值的总和是

如果我们的集合是拟阵,那么我们的答案是 l 中的最大集合 A,其中没有 x 可以进一步添加

为了检查,我们看看拟阵的性质是否在集合 S = 25,15,1 中成立,其中 V = 30 现在,l 中有两个子集: A = 25 和 B= 15,15 由于 |A| B-A 使得 A U x ->l (根据 3) 所以,25,15 应该属于 l,但它是一个矛盾,因为 25+15>30

所以,S 不是拟阵,因此贪心方法对它不起作用。

【讨论】:

这个说法不正确。如果 S = 25,10,5,1, V = 30, A=25, B=10,10,10,同样的论证表明 S,I 不是拟阵,因为 25, 10) 不在 I 中。另一方面,贪心算法适用于 S 的这种选择(如 CLRS,问题 16-1a 中所示)。某个拟阵结构的存在是贪心算法正确性的充分但非必要条件。 @TobiasHagge 我们是否有一个条件告诉我们贪婪算法对于一个集合会失败?【参考方案2】:

在任何情况下,如果没有硬币的价值在加到最低面额时低于该面额的两倍,那么贪心算法就会起作用。

即1,2,3 有效,因为 [1,3] 和 [2,2] 添加到相同的值 但是 1, 15, 25 不起作用,因为(对于更改 30)15+15>25+1

【讨论】:

不错的答案。这就是我一直在寻找的:) 通过测试保证贪婪方法有效,但反之则不然。贪婪适用于 1、5、15、25。 这个答案似乎是错误的。即使 8 + 8 = 16 我不关注,这个答案是完全错误的吗?为什么这有这么多的赞成票?我错过了什么吗?【参考方案3】:

如果贪心算法给出的硬币数量对于所有数量都是最优的,那么硬币系统就是规范的。

This paper offers an O(n^3) algorithm for deciding whether a coin system is canonical, where n is the number of different kinds of coins.

对于非规范硬币系统,有一个数量c,贪婪算法会产生一个次优数量的硬币; c 被称为反例。如果最小的反例大于最大的单个硬币,则硬币系统是紧的。

【讨论】:

它还引用了其他测试,包括最小的反例必须低于两个最大硬币的总和。【参考方案4】:

这是一个重复出现的问题。给定一组硬币Cn, Cn-1, . . ., 1,对于1 <= k <= n, Ck > Ck-1,如果 Ck > Ck-1 + Ck-2 和值V=(Ck + Ck-1) - 1,贪婪算法将产生最小数量的硬币,将贪婪算法应用于硬币子集Ck, Ck-1, . . ., 1,其中Ck <= V,产生的硬币数量少于将贪婪算法应用于硬币子集Ck-1, Ck-2, . . ., 1 所产生的数量。

测试很简单:对于 `1

例如,当 n=4 时,考虑硬币集合 C4, C3, C2, 1 = 50,25,10,1。从 k=n=4 开始,然后 V = Cn + Cn-1 - 1 = 50+25-1 = 74 作为测试值。对于 V=74,G50,25,10,1 = 7 个硬币。 G25, 10, 1 = 8 个硬币。到目前为止,一切都很好。现在让 k=3。那么V=25+10-1=34。 G25, 10, 1 = 10 个硬币,但 G10, 1 = 7 个硬币。因此,我们知道贪心算法不会最小化硬币集 50,25,10,1 的硬币数量。另一方面,如果我们在这个硬币组中添加镍,G25, 10, 5, 1 = 6 和 G10, 5, 1 = 7。同样,对于 V=10+5-1= 14,我们得到 G10, 5, 1 = 5,但 G5,1 = 6。所以,我们知道,Greedy 适用于 50,25,10,5,1。

这引出了一个问题:硬币的面额应该是什么,满足贪婪算法,这会导致从 1 到 100 的任何值的硬币的最小最坏情况数?答案很简单:100 个硬币,每个硬币的值从 1 到 100 不同。可以说这不是很有用,因为它在每笔交易中线性搜索硬币。更不用说铸造这么多不同面额并跟踪它们的费用了。

现在,如果我们首先要最小化面额的数量,而其次要最小化 Greedy 产生的从 1 到 100 的任何值的硬币的最终数量,那么面额为 2 的硬币:64, 32, 16, 8, 4, 2, 1 对于任何 1:100 的值(值小于十进制 100 的 7 位数字中的最大 1 数),最多产生 6 个硬币。但这需要 7 个面额的硬币。五个面额 50, 25, 10, 5, 1 的最坏情况是 8,这发生在 V=94 和 V=99。 3 1, 3, 9, 27, 81 次方的硬币也只需要 5 个面额即可被 Greedy 使用,但在最坏的情况下也会产生 8 个面值分别为 62 和 80 的硬币。最后,使用任何 5 个面额64, 32, 16, 8, 4, 2, 1 的子集不能排除 '1',并且满足 Greedy,也将产生最多 8 个硬币。所以存在线性权衡。将面额的数量从 5 增加到 7 会将表示 1 到 100 之间的任何值所需的最大硬币数量分别从 8 减少到 6。

另一方面,如果你想尽量减少买卖双方之间交换的硬币数量,假设每个人的口袋里至少有一个每种面额的硬币,那么这个问题是相当于平衡从 1 磅到 N 磅的任何重量所需的最少重量。事实证明,如果硬币面额是 3 的幂:1, 3, 9, 27, . . .

见https://puzzling.stackexchange.com/questions/186/whats-the-fewest-weights-you-need-to-balance-any-weight-from-1-to-40-pounds。

【讨论】:

【参考方案5】:

好吧,我们真的需要重新表述这个问题......贪婪算法本质上是它试图使用提供的硬币面额获得目标值。您对贪心算法所做的任何更改都只会更改达到目标值的方式。 它不考虑使用的最低硬币.... 为了更好地解决这个问题,不存在安全措施。 较高面额的硬币可能会迅速产生目标价值,但这不是一个安全的举动。 示例 50,47,51,2,9 获得 100 贪婪的选择是拿最高面额的硬币更快地达到 100.. 51+47+2 好吧,它达到了,但应该可以达到 50+50..

让我们用 50,47,51,9 得到 100 如果它贪婪地选择最高的硬币 51 它需要 49 从集合。它不知道这是否可能。它试图达到 100 但它不能 改变贪婪的选择只会改变达到 100 的方式 这些类型的问题创建了一组解决方案和决策树分支的形式。

【讨论】:

【参考方案6】:

理论:

如果贪心算法总是为给定的一组硬币产生最佳答案,那么你说该组是规范的

尽可能简洁地说明best known algorithmic test [O(n^3)] for determining whether an arbitrary set of n coins is canonical:

[c1,c2,..cn] is canonical iff for all w_ij |G(w_ij)| = |M(w_ij)|, 1 < i <= j <= n 

其中[c1,c2,...cn] 是按cn = 1 降序排列的硬币面额列表

G(x)表示在输入x上运行贪心算法的硬币向量结果,(返回为[a1, a2,..., an],其中aici的计数)

M(x) 表示x 使用最少硬币的硬币矢量表示

|V|表示硬币向量的大小V:向量中硬币的总数

w_ijG(c_(i-1) - 1) 在将其j'th 个硬币增加1 并将其所有硬币计数从j+1 归零到n 之后产生的硬币向量的评估值。

实现(JavaScript):

/**
 * Check if coins can be used greedily to optimally solve change-making problem
 * 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...] sorted descending with cn = 1 => 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.")

【讨论】:

【参考方案7】:

今天,我在 Codeforces 上解决了类似的问题(链接将在最后提供)。 我的结论是,要通过贪心算法解决硬币找零问题,它应该满足以下条件:-

1.按升序排序硬币值时,大于当前元素的所有值都应能被当前元素整除。

例如硬币 = 1, 5, 10, 20, 100,这将给出正确答案,因为 5,10, 20, 100 都可以被 1 整除,10,20, 100 都可以被 5 整除,20,100 都可以被 10 整除,100 都可以被 20 整除。

希望这能提供一些想法。

996 A - 中彩票 https://codeforces.com/blog/entry/60217

【讨论】:

1 2 5 10 20 50 100 怎么样?【参考方案8】:

一个容易记住的例子是任何一组硬币,如果它们按升序排序并且你有:

coin[0] = 1
coin[i+1] >= 2 * coin[i], for all i = 0 .. N-1 in coin[N]

然后使用这种硬币的贪心算法将起作用。

根据您查询的范围,可能会有更优化的(就所需硬币数量而言)分配。例如,如果您正在考虑范围 (6..8) 并且您有硬币 而不是 。

在 N+ 上完成的最有效的硬币分配是在上述规则集相等的情况下,给你硬币 1、2、4、8 ...;这仅仅是任何数字的二进制表示。从某种意义上说,base 之间的对话本身就是一种贪心算法。

Max Rabkin 在此讨论中提供了关于 >= 2N 不等式的证明: http://olympiad.cs.uct.ac.za/old/camp0_2008/day1_camp0_2008_discussions.pdf

【讨论】:

这是一个有趣的链接,但它所证明的只是,如果一组具有最大硬币 m 的硬币是非贪婪的,那么贪婪和最优解决方案给出的总和一定是 = 2 倍下一个 -最大,但我没看到。 除了您的链接证明与您声称的不同之外,您声称它证明的事情是错误的:硬币集 25, 10, 1 遵守您的“至少是前一个硬币的两倍”条件,但是对于总共30个,贪心算法会给出25+5*1(6个硬币),而最优解是3*10(3个硬币)。 -1. 贪心算法确实提供了一个有效的答案(25 + 1 + 1 + 1 + 1 + 1),但不是最有效的。 OP 的问题清楚地表明他/她打算“工作”意味着“使用最少数量的硬币”。 (顺便说一句,如果您要求硬币套装包括 1 美分硬币,那么任何 选择硬币的方法不会导致总金额超过目标金额将按照您的较低标准“工作” “使用任意数量的硬币产生正确的变化”,所以贪婪不会给你买任何东西。)

以上是关于为啥贪心硬币找零算法对某些硬币组不起作用?的主要内容,如果未能解决你的问题,请参考以下文章

硬币找零问题的动态规划实现

贪心算法算法导论 找零问题

贪心算法——找零钱问题

贪心算法

数据结构与算法(12)—分治策略

重叠在 Phaser 中使用 JavaScript 不起作用