Algorithm 寻找硬币组合以产生给定变化的错误递归方法
我最近在做一个项目euler问题(即#31),基本上是找出我们可以用集合{1,2,5,10,20,50100200}的元素求和到200的方法 我使用的想法是:求和N的方法数等于 (求和N-k的方法数)*(求和k的方法数),求和k的所有可能值 我意识到这种方法是错误的,也就是说,由于它产生了几个重复计数。我试图调整公式以避免重复,但没有效果。我正在寻求堆栈溢出者关于以下方面的智慧:Algorithm 寻找硬币组合以产生给定变化的错误递归方法,algorithm,recursion,Algorithm,Recursion,我最近在做一个项目euler问题(即#31),基本上是找出我们可以用集合{1,2,5,10,20,50100200}的元素求和到200的方法 我使用的想法是:求和N的方法数等于 (求和N-k的方法数)*(求和k的方法数),求和k的所有可能值 我意识到这种方法是错误的,也就是说,由于它产生了几个重复计数。我试图调整公式以避免重复,但没有效果。我正在寻求堆栈溢出者关于以下方面的智慧: 我的递归方法是否涉及要解决的正确子问题 如果存在,那么消除重复的有效方法是什么 我们应该如何处理递归问题,以便关注正
当试图避免重复排列时,在大多数情况下有效的简单策略是只创建上升或下降序列 在您的示例中,如果您选择一个值,然后对整个集合进行递归,您将得到重复的序列,如
50,50100
和50100,50
和100,50,50
。但是,如果使用下一个值应等于或小于当前选定值的规则进行递归,则在这三个值中,您将只获得序列100,50,50
因此,只计算唯一组合的算法为:
function uniqueCombinations(set, target, previous) {
for all values in set not greater than previous {
if value equals target {
increment count
}
if value is smaller than target {
uniqueCombinations(set, target - value, value)
}
}
}
uniqueCombinations([1,2,5,10,20,50,100,200], 200, 200)
或者,可以在每次递归之前创建集合的副本,并从中删除不希望重复的元素
上升/下降序列方法也适用于迭代。假设您希望找到三个字母的所有唯一组合。此算法将打印结果,如a,c,e
,但不打印a,e,c
或e,a,c
:
for letter1 is 'a' to 'x' {
for letter2 is first letter after letter1 to 'y' {
for letter3 is first letter after letter2 to 'z' {
print [letter1,letter2,letter3]
}
}
}
m69给出了一个很好的策略,它通常是有效的,但我认为更好地理解它为什么有效是值得的。当尝试计数项目(任何类型)时,一般原则是: 考虑一个规则,将任何给定的项目准确地划分为几个不重叠的类别中的一个。也就是说,列出一个具体类别a、B、…、Z的列表,这将使下面的句子成为事实:一个项目要么在类别a中,要么在类别B中,要么在类别Z中 完成此操作后,您可以安全地计算每个类别中的项目数量,并将这些数量相加,因为(a)在一个类别中计算的任何项目都不会在任何其他类别中再次计算,(b)您想要计算的任何项目都在某个类别中(即,没有遗漏) 我们如何在这里为您的具体问题建立分类?一种方法是注意每一个项目(即,每多组硬币价值总和为所需的总N)要么包含50枚硬币正好零次,要么包含它正好一次,或者包含它正好两次,或者…,或者包含它正好四舍五入(N/50)次。这些类别并不重叠:例如,如果一个解决方案使用的硬币正好是5 50个,那么它显然也不能使用7 50个硬币。此外,每个解决方案显然都属于某个类别(请注意,对于不使用50枚硬币的情况,我们包含了一个类别)。因此,如果我们有一种方法来计算,对于任何给定的k,使用集合{1,2,5,10,20,50100200}中的硬币来产生一个N的和,并使用精确的k50硬币的解的数量,那么我们可以将所有的k从0到N/50求和,得到一个精确的计数 如何有效地做到这一点?这就是递归的用武之地。使用集合{1,2,5,10,20,50100200}中的硬币来产生一个N的总和并精确使用K50硬币的解决方案的数量等于总和为N-50k且不使用任何50个硬币的解决方案的数量,即仅使用集合{1,2,5,10,20100200}中的硬币。这当然适用于我们可以选择的任何特定硬币面额,因此这些子问题与原始问题具有相同的形状:我们可以通过任意选择另一种硬币(例如10枚硬币)来解决每个问题,并根据这种新硬币形成一组新的类别,计算每个类别中的项目数量并进行汇总。子问题变得越来越小,直到我们达到一些我们直接处理的简单基本情况(例如,不允许留下硬币:如果N=0,则有1个项目,否则为0个项目)
我从50枚硬币(而不是最大或最小的硬币)开始强调,用于形成非重叠类别集的特定选择与算法的正确性无关。但在实践中,传递一套硬币的显式表示是不必要的昂贵。因为我们实际上并不关心用于形成类别的特定货币序列,所以我们可以自由选择更有效的表示形式。在这里(以及在许多问题中),可以方便地将允许的硬币集隐式表示为单个整数maxCoin,我们将其解释为原始硬币有序列表中的第一个maxCoin硬币是允许的。这限制了我们可以表示的可能集合,但这没关系:如果我们总是选择最后一个允许的硬币来形成类别,我们可以通过简单地将参数maxCoin-1传递给它,将新的、更受限制的允许硬币“集合”非常简洁地传递给子问题。这是m69答案的精髓。这里有一些很好的指导。另一种思考方法是将其视为动态程序。为此,我们必须将问题作为选项中的一个简单决定来提出,从而使我们对同一问题有一个较小的版本。它归结为某种递归表达式 把公司
W(i,v) = sum_[k = 0..floor(200/ci)] W(i+1, v-ci*k)
W(n-1,v) = 1 if v % c_(n-1) == 0 and 0 otherwise.
#include <stdio.h>
#define n 8
int cv[][n] = {
{200,100,50,20,10,5,2,1},
{1,2,5,10,20,50,100,200},
{1,10,100,2,20,200,5,50},
};
int *c;
int w(int i, int v) {
if (i == n - 1) return v % c[n - 1] == 0;
int sum = 0;
for (int k = 0; k <= v / c[i]; ++k)
sum += w(i + 1, v - c[i] * k);
return sum;
}
int main(int argc, char *argv[]) {
unsigned p;
if (argc != 2 || sscanf(argv[1], "%d", &p) != 1 || p > 2) p = 0;
c = cv[p];
printf("Ways(%u) = %d\n", p, w(0, 200));
return 0;
}
$ ./foo 0
Ways(0) = 73682
$ ./foo 1
Ways(1) = 73682
$ ./foo 2
Ways(2) = 73682