Algorithm 寻找硬币组合以产生给定变化的错误递归方法

Algorithm 寻找硬币组合以产生给定变化的错误递归方法,algorithm,recursion,Algorithm,Recursion,我最近在做一个项目euler问题(即#31),基本上是找出我们可以用集合{1,2,5,10,20,50100200}的元素求和到200的方法 我使用的想法是:求和N的方法数等于 (求和N-k的方法数)*(求和k的方法数),求和k的所有可能值 我意识到这种方法是错误的,也就是说,由于它产生了几个重复计数。我试图调整公式以避免重复,但没有效果。我正在寻求堆栈溢出者关于以下方面的智慧: 我的递归方法是否涉及要解决的正确子问题 如果存在,那么消除重复的有效方法是什么 我们应该如何处理递归问题,以便关注正

我最近在做一个项目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