不正确的递归方法来查找硬币组合以产生给定的变化

时间:2015-10-29 08:20:24

标签: algorithm recursion

我最近在做一个项目euler问题(即#31),它基本上找出了使用集合{1,2,5,10,20,50,100,200}的元素可以总和到200的方法。

我使用的想法是:总和为N的方式的数量等于

  

(求和N-k的方式的数量)*(求和k的方式的数量),对k的所有可能值求和。

我意识到这种方法是错误的,即由于它创建了几个重复计数。我试图调整公式以避免重复,但无济于事。我正在寻求关于堆栈溢出的智慧:

  1. 我的递归方法是否与正确要解决的子问题有关
  2. 如果存在,那么什么是消除重复的有效方法
  3. 我们应该如何处理递归问题,以便我们关注正确的子问题?我们选择了一个正确(或不正确)子问题的指标是什么?

3 个答案:

答案 0 :(得分:3)

当试图避免重复排列时,在大多数情况下有效的直接策略是仅创建上升或下降序列。

在您的示例中,如果您选择一个值,然后使用整个集合进行递归,您将获得重复的序列,例如50,50,10050,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,ce,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