我最近在做一个项目euler问题(即#31),它基本上找出了使用集合{1,2,5,10,20,50,100,200}的元素可以总和到200的方法。
我使用的想法是:总和为N的方式的数量等于
(求和N-k的方式的数量)*(求和k的方式的数量),对k的所有可能值求和。
我意识到这种方法是错误的,即由于它创建了几个重复计数。我试图调整公式以避免重复,但无济于事。我正在寻求关于堆栈溢出的智慧:
答案 0 :(得分:3)
当试图避免重复排列时,在大多数情况下有效的直接策略是仅创建上升或下降序列。
在您的示例中,如果您选择一个值,然后使用整个集合进行递归,您将获得重复的序列,例如50,50,100
和50,100,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]
}
}
}
答案 1 :(得分:2)
m69提供了一个经常有效的好策略,但我认为值得更好地理解为什么它的工作原理。当试图计算(任何类型)项目时,一般原则是:
考虑将任何给定项分类为几个非重叠类别中的的规则。也就是说,想出一个将使以下句子成立的具体类别A,B,...,Z的列表:项目在类别A中,或在类别B中,或......中,或在类别Z中
完成此操作后,您可以安全地计算每个类别中的项目数量并将这些计数加在一起,并且知道以下知识:(a)在一个类别中计算的任何项目不会再计入任何其他类别,和(b)您想要计算的任何项目都在某些类别中(即,没有错过)。
我们如何在此处为您的特定问题制作类别?一种方法是注意每个项目(即,每个硬币值的多个集合总和为所需的总N)要么恰好零次包含50个硬币,要么恰好包含它一次,或者它包含它恰好两次,或者......,或者它包含完全RoundDown(N / 50)次。这些类别不重叠:如果一个解决方案恰好使用了5个50个硬币,那么它显然也不能正好使用7个50个硬币。此外,每种解决方案显然都属于某种类别(请注意,我们在其中包含不使用50个硬币的情况的类别)。因此,如果我们有办法计算,对于任何给定的k,使用集合{1,2,5,10,20,50,100,200}中的硬币生成N 之和并使用k的解决方案的数量50个硬币,然后我们可以将所有k从0加到N / 50并获得准确的计数。
如何有效地做到这一点?这就是递归的来源。使用集合{1,2,5,10,20,50,100,200}中的硬币产生N的总和并使用恰好k 50个硬币的解决方案的数量等于总和为N-50k 并且不使用任何50个硬币的解决方案,即仅使用来自集合{1,2,5,10,20,100,200}的硬币。这当然适用于我们可以选择的任何特定硬币面额,因此这些子问题具有与原始问题相同的形状:我们可以通过简单地任意选择另一个硬币(例如10个硬币)来解决每个硬币,形成一个新的集合基于这个新硬币的类别,计算每个类别中的项目数量并将它们相加。子问题变得更小,直到我们达到一个我们直接处理的简单基本情况(例如,没有允许的硬币剩下:如果N = 0则有1个项目,否则有0个项目。)
我从50枚硬币(而不是最大或最小的硬币)开始,强调用于形成一组非重叠类别的特定选择对于它的正确性并不重要。算法。但实际上,通过硬币组的明确表示是不必要的昂贵。由于我们实际上并不关心用于形成类别的特定硬币序列,因此我们可以自由选择更有效的表示。在这里(以及许多问题),将允许的硬币隐含地的集合简单地表示为单个整数maxCoin是方便的,我们将其解释为意味着原始的第一个maxCoin硬币有序的硬币列表是允许的。这限制了我们可以表示的可能的集合,但是在这里可以:如果我们总是选择最后允许的硬币来形成类别,我们可以传达新的,更受限制的"集合"通过简单地将参数maxCoin-1传递给它,允许硬币到子问题非常简洁。这是m69答案的精髓。
答案 2 :(得分:0)
这里有一些很好的指导。另一种思考方式是作为动态程序。为此,我们必须将问题作为选项之间的简单决策,使我们留下相同问题的较小版本。它归结为某种递归表达式。
按照您喜欢的顺序放置硬币值c0,c1,... c_(n-1)。然后将W(i,v)定义为使用硬币ci,c_(i + 1),... c_(n-1)可以对值v进行更改的方式。我们想要的答案是W(0,200)。剩下的就是定义W:
W(i,v) = sum_[k = 0..floor(200/ci)] W(i+1, v-ci*k)
用语言来说:我们可以用硬币ci向前改变的方式是在决定使用一些可行数k的硬币ci之后总结我们可以改变的所有方法,从问题中去除那么多的价值。
当然我们需要递归的基本情况。当i = n-1:最后一个硬币值时会发生这种情况。在这一点上,当且仅当我们需要的值是c_(n-1)的精确倍数时,才有一种改变的方法。
W(n-1,v) = 1 if v % c_(n-1) == 0 and 0 otherwise.
我们通常不希望将其实现为简单的递归函数。相同的参数值重复出现,这导致指数(n和v)量的浪费计算。有简单的方法可以避免这种情况。表格评估和记忆是两个。
另一点是 更有效地使值按降序排列。通过尽早获取大块值,递归评估的总数最小化。另外,由于c_(n-1)现在是1,所以基本情况只是W(n-1)= 1。现在很明显我们可以添加第二个基本案例作为优化:W(n-2,v) = floor(v/c_(n-2))
。这是for
循环总和W(n-1,1)= 1的次数!
但这是一个礼貌的礼貌。问题是如此之小,以至于指数行为并不意味着。这是一个小实现,表明订单确实无关紧要:
#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;
}
Drumroll,拜托......
$ ./foo 0
Ways(0) = 73682
$ ./foo 1
Ways(1) = 73682
$ ./foo 2
Ways(2) = 73682