我了解贪心算法用于硬币找零问题(以最少的硬币支付特定金额)的工作原理-它总是选择面额不超过剩余总额的最大硬币,并且它始终可以找到特定硬币集合的正确解决方案。
但是对于某些硬币集合,存在一些总和使得贪心算法无法找到最优解。例如,对于集合{1、15、25}
和总和30,贪心算法首先选择25,剩下5元,然后选五个1元硬币,总共需要六枚硬币。但是实际上最少的硬币数解决方案是选择15两次。
硬币集合必须满足什么条件才能使贪心算法在所有总额中找到最小解?
我了解贪心算法用于硬币找零问题(以最少的硬币支付特定金额)的工作原理-它总是选择面额不超过剩余总额的最大硬币,并且它始终可以找到特定硬币集合的正确解决方案。
但是对于某些硬币集合,存在一些总和使得贪心算法无法找到最优解。例如,对于集合{1、15、25}
和总和30,贪心算法首先选择25,剩下5元,然后选五个1元硬币,总共需要六枚硬币。但是实际上最少的硬币数解决方案是选择15两次。
硬币集合必须满足什么条件才能使贪心算法在所有总额中找到最小解?
在硬币找零问题中, S是按价值从大到小排序的所有硬币组成的集合。 我们需要用S中最少的硬币数量达到价值V。
在我们的情况下,l是一个包含所有子集的独立集,满足它们的价值之和<=V。
如果我们的集合是一个拟阵,那么我们的答案就是在l中不能再加入任何元素的最大集合A。
为了验证,我们观察是否满足拟阵的性质,在S={25,15,1}, V=30的情况下。 现在有两个l中的子集: A={25}和B={15,15} 由于|A| < |B|,根据3的条件,我们可以找到x属于B-A,使得(A并{x})属于l。 所以{25,15}应该属于l,但是这是一个矛盾,因为25+15>30。
因此,S不是拟阵,因此贪心算法无法在其上运行。
在任何没有硬币的情况下,其价值加上最小面额仍低于比它小一级的面额的两倍时,贪心算法都是适用的。
例如,{1,2,3} 是适用的,因为[1,3]和[2,2]相加的结果相同。 然而,{1,15,25} 不适用,因为(对于找零30),15 + 15>25 + 1。
一个硬币系统是规范的,如果贪心算法找零所给出的硬币数量对于所有金额都是最优的。
本论文提供了一个 O(n^3) 的算法来判断一个硬币系统是否规范,其中 n 是不同种类硬币的数量。
对于一个非规范的硬币系统,存在一个金额 c
,贪心算法得到的硬币数量不是最优的;c
被称为反例。重要的是,非规范的硬币系统将有一个 c
,它比两个最大硬币的面值之和要小,因此可以在有限时间内找到它。
另外,一个硬币系统被称为 "紧密的",如果它的最小反例大于最大的单个硬币。
{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, 3, 9, 27, . . .}
。理论:
如果贪心算法总是针对给定的硬币集合产生最优解,则称该集合为规范。
以下是关于确定任意n个硬币集合是否规范的已知最佳算法测试 [O(n^3)],简洁地陈述如下:
[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]
,其中 ai
是 ci
的计数)
M(x)
表示使用最少硬币的硬币向量表示 x
|V|
表示硬币向量 V
的大小:向量中硬币的总数
而 w_ij
是通过将其 j
号硬币加1并将其从 j+1
到 n
的所有硬币计数归零后,由 G(c_(i-1) - 1)
产生的硬币向量的计算值。
实现(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.")
一个容易记住的案例是,任何一组硬币,如果它们按升序排序并且你有:
coin[0] = 1
coin[i+1] >= 2 * coin[i], for all i = 0 .. N-1 in coin[N]
那么使用这些硬币的贪心算法将会起作用。
根据您查询的范围,可能会有更优化(以所需硬币数量为准)的分配方式。例如,如果您考虑范围(6..8),并且您拥有硬币<6、7、8>而不是<1、2、4、8>。
最有效的硬币分配是在N+上完全平等地遵循上述规则,给您硬币1、2、4、8...;这仅仅是任何数字的二进制表示。在某种意义上,进制之间的转换本身就是一种贪心算法。
关于 >= 2N 不等式的证明由Max Rabkin在此讨论中提供: http://olympiad.cs.uct.ac.za/old/camp0_2008/day1_camp0_2008_discussions.pdf
{25, 10, 1}
符合你所说的“至少是前一个硬币面值的两倍”条件,但对于总额为30的情况,贪心算法会给出25+51(6枚硬币)的结果,而最优解是310(3枚硬币)。-1。 - j_random_hacker