C++子集和2^n/递归错误/澄清

C++ subset sum 2^n/recursion bug/clarification

本文关键字:递归 错误 澄清 子集 C++      更新时间:2023-10-16

这不是家庭作业,我没有钱上学,所以我在高速公路上的收费站轮班时自学(晚上很长,顾客很少)。

我正在尝试实现一个简单的子集求和算法,该算法在给定一个整数数组的情况下,返回一个子集,其总和等于所需的总和,并报告需要调用多少次才能找到它。

我使用Collections在Java中实现了一个,但这是一个非常臃肿的代码,即使我能够返回所有集合加起来达到所需的数字,并告诉函数是否在第一次匹配时停止。

我对这段代码的问题如下:它不是在2^n时间内运行(对于这样一个没有结果的实现来说,这是正确的,不是吗?),而是在[2^(n+1)]-1时间内运行;O(2^n)如评论所指出的。我明白为什么我会在比我能做的更深的层次上检查(runningTotal==targetTotal),本质上是自己增加了额外的深度,不是吗?我试图尽可能干净地对基本情况进行建模,如果您检测到任何"代码气味",请告诉我。我应该一看到就崩溃吗(runningTotal+consider)==targetTotal?

注意:我不认为这属于"代码审查",因为我问的是特定的代码行,而不是整体方法(如果我需要改变方法,那就顺其自然,我这样做是为了学习)。

这里是我的尝试(除了上面提到的缺乏优化之外,这个"还可以"的C/C++吗?):

#include <iostream>
using namespace std;
bool setTotalling(int chooseFrom[], int nChoices, int targetTotal,
    int chooseIndex, int runningTotal, int solutionSet[], int &solutionDigits,
    int &nIterations) {
  nIterations++;
  if (runningTotal == targetTotal) {
    return true;
  }
  if (chooseIndex >= nChoices) {
    return false;
  }
  int consider = chooseFrom[chooseIndex];
  if (setTotalling(chooseFrom, nChoices, targetTotal, chooseIndex + 1,
      runningTotal + consider, solutionSet, solutionDigits, nIterations)) {
    solutionSet[solutionDigits++] = consider;
    return true;
  }
  if (setTotalling(chooseFrom, nChoices, targetTotal, chooseIndex + 1,
      runningTotal, solutionSet, solutionDigits, nIterations)) {
    return true;
  }
  return false;
}
void testSetTotalling() {
  int chooseFrom[] = { 1, 2, 5, 9, 10 };
  int nChoices = 5;
  int targetTotal = 23;
  int chooseIndex = 0;
  int runningTotal = 0;
  int solutionSet[] = { 0, 0, 0, 0, 0 };
  int solutionDigits = 0;
  int nIterations = 0;
  cout << "Looking for a set of numbers totalling" << endl << "--> "
      << targetTotal << endl << "choosing from these:" << endl;
  for (int i = 0; i < nChoices; i++) {
    int n = chooseFrom[i];
    cout << n << ", ";
  }
  cout << endl << endl;
  bool setExists = setTotalling(chooseFrom, nChoices, targetTotal, chooseIndex,
      runningTotal, solutionSet, solutionDigits, nIterations);
  if (setExists) {
    cout << "Found:" << endl;
    for (int i = 0; i < solutionDigits; i++) {
      int n = solutionSet[i];
      cout << n << ", ";
    }
    cout << endl;
  } else {
    cout << "Not found." << endl;
  }
  cout << "Iterations: " << nIterations << endl;
}
int main() {
  testSetTotalling();
  return 0;
}

重点是如何计算"迭代"。假设您有一个简单的例子,n=1的目标是一个不为零的和,而不是您所拥有的元素。

你调用函数,这会立即增加计数器,然后你到达分支,函数调用自己两次(一次考虑元素,一次不考虑元素)。这些呼叫中的每一个都将计数为1,因此您最终将得到一个总数为3的计数器。

我看不出有什么问题。。。

如果剩余选项的麻木为零,您可以添加一个特殊的检查来重复测试并避免调用,但这需要重复检查。只在递归调用处进行结束检查不会考虑到可以直接使用零选择来调用函数。基本上,您正在"内联"级别0……但为什么要停止在级别0,而不内联级别1呢?

如果你正在寻找加速,请注意(假设所有元素都是非负的)如果你知道添加所有剩余的可用数字仍然不足以达到目标,那么你可以避免检查所有可能的子集。通过计算一次从给定索引到可用元素列表末尾的所有剩余数字的总和(这是O(n)计算),您可以保存(剩余2^)迭代。此外,如果当前的总和已经太大,那么考虑添加其他元素也没有意义。

if (targetTotal > runningTotal)
    return false; // We already passed the limit
if (targetTotal - runningTotal > sumOfAllFrom[choseIndex])
    return false; // We're not going to make it

如果你也按递减顺序对元素进行排序,上述优化可以节省很多。