【问题标题】:Replacing N-level for loops with recursion in C++在 C++ 中用递归替换 N 级 for 循环
【发布时间】:2017-12-11 04:13:55
【问题描述】:

一段时间以来,我一直在尝试想出一种方法来计算单词字符串的所有各种组合。然而,与网络上的大多数组合方法不同,该算法必须产生每个组合,包括所有组合元素都不是一个组合的组合。即,如果我将“Hello”、“New”和“World”组合在一起,我正在寻找的组合是:

HelloNewWorld
HelloNew
HelloWorld
Hello
NewWorld
New
World

我大学的一位教授为此提出了一个快速而肮脏的解决方案,但它使用嵌套的 for 循环。

#include <iostream>
#include <vector>
#include <array>
#include <string>

int main()
{
std::vector<std::array<std::string, 2>> vec(3);
vec[0] = {"Hello", ""};
vec[1] = {"New", ""};
vec[2] = {"World", ""};
for (int i = 0; i < 2; i++)
    for (int j = 0; j < 2; j++)
        for (int k = 0; k < 2; k++)
            std::cout << vec[0][i] + vec[1][j] + vec[2][k] << std::endl;
}

正如您可能想象的那样,我想要一种方法来使它实际上有点可用和便携。我知道这可以通过递归实现,我只是不知道如何实现它。最理想的情况是,如果可能的话,我想使这个尾递归,因为计划是计算非常大的组合。递归执行此操作的最佳方法是什么,尾递归是否容易?

【问题讨论】:

  • 其中一项要求是它们必须是单词,还是严格的字符组合?
  • 我强烈建议您使用 Howard Hinnant 的作品在 C++ 中创建高效的组合库。它们是递归的,但它们快速、便携且易于使用。在这种情况下,使用std::string s = "ab"; for (size_t i =0; i &lt;= ab.size(); ++i) { for_each_combination(s.begin(), s.begin()+i, s.end(), f); } howardhinnant.github.io/combinations.html
  • @M4rc 要求必须是单词。抱歉,我应该在我的帖子中这么说。
  • 如果您需要一个真正的 N 级 for 循环(笛卡尔积),可以使用很好的实现。披露:我写了这个:gist.github.com/Alexhuszagh/67efb078a82616ed07529ed97586646a
  • @AlexanderHuszagh Howard Hinnat 的组合库看起来非常有趣,而且您的代码看起来很适合它的功能,但它们都不会给我想要的输出。这两个选项都没有提供与某些组合中缺少的组合元素的组合。这是我知道如何描述它的最好方式。我编辑了我的帖子,希望能澄清我在寻找什么。

标签: c++ for-loop recursion tail-recursion


【解决方案1】:

如果一个词出现或不出现的唯一可能性,那就有两种可能性。因此,对于 n 个单词,您有 2^n 个组合。因此,您只需计算从 0(包括)到 2^n-1(包括)的 2^n 个数字,并将每个位映射到一个单词。

不需要递归,只需要一个循环。

【讨论】:

    【解决方案2】:

    如果我理解正确,您想生成字符串的所有组合。在这种情况下,您可以使用 BFS 以及集合和队列来生成组合,我将尝试解释。

    假设你的字符串是ABCD。您有一个要添加 ABCD 的队列和一个要添加 ABCD 的集合

    while the queue is not empty
    1) you pop the top element
    2) you generate substrings of that popped element
      a) if that substring is not in the set add it to the queue
    

    要在步骤 2 中生成子字符串,请执行以下操作

    for(int i =0;i<string.length();i++)
    {
      string substr1 = string.substr(0,i);
      string substr2 = string.substr(i,string.length()-1);
      string substring = substr1+substr2;
    }
    

    ABCD(输入字符串)执行此操作将生成BCDACDABDABC。现在将这 3 个添加到集合和队列中

    现在,您将 BCDACDABD 添加到集合中。假设BCDqueue.front()。你弹出它并生成CDBDBC 并将它们添加到set 和队列中。当您接下来弹出ACD 时,您会生成CDADAC,但现在您不会将CD 添加到队列中,因为它在集合中。

    编辑

    我看到了您的问题,我的答案适用于字符串,但您可以在 vector&lt;string&gt; 上使用相同的原理来生成所有组合 ABCD 将只是Hello(A)World(B)...

    【讨论】:

      【解决方案3】:

      您可以通过对包含 N 个元素的向量使用从 k=1k=N 的所有组合非常有效地做到这一点。使用可用的 Howard Hinnant 库here,您可以相当有效地使用它。就我而言,我将库命名为 sampling.h,这是唯一的外部依赖项,可以完整查看 here

      #include "sampling.h"
      #include <iostream>
      #include <vector>
      
      
      /**
       *  This function can take any container that has a bidirectional
       *  iterator (std::list, std::deque, std::vector) that contains elements
       *  of type std::string or similar, that must implement an `operator+`
       *  and `operator<<` for printing.
       */
      template <typename BiDirStringContainer>
      void print_combinations(BiDirStringContainer& container)
      {
          auto first = container.begin();
          auto last = container.end();
          for (size_t i = 1; i <= container.size(); ++i) {
              auto mid = first + i;
              for_each_combination(first, mid, last, [](auto f, auto l) {
                  std::string w;
                  for (; f != l; ++f) {
                      w += *f;
                  }
                  std::cout << w << std::endl;
                  return false;
              });
          }
      }
      
      
      int main(void)
      {
          std::vector<std::string> words = {
              "Hello",
              "New",
              "World",
          };
          print_combinations(words);
      
          return 0;
      }
      

      使用 C++14 标准编译并运行它会输出:

      Hello
      New
      World
      HelloNew
      HelloWorld
      NewWorld
      HelloNewWorld
      

      这正是您的帖子所描述的。由于 lambda 是一个自定义函子,并且可以存储状态,因此您可以对组合做任何您想做的事情:存储副本、打印它们等等。

      这比您在标准库中获得的任何东西都快得多,而无需进行大量工作,或者从为标准库提出的建议中获得。例如std::next_combinationstd::next_permutation(前者不包括在内,但建议here)。我强烈建议阅读 Howard Hinnant 的完整博客文章:它很有启发性。他的实现的时间复杂度和速度超过了大多数其他建议。如果您需要高性能的组合或排列,他已经为您完成了这项工作。

      【讨论】:

      • 感谢您发布代码。我很快浏览了你之前提到的博客文章,并错误地认为,由于前几个示例看起来产生了经典意义上的组合,他的库不适合我的需要。但是这个库似乎用途很广,谢谢提及!我一定会更多地研究它。感谢您的帮助!
      【解决方案4】:

      在每个级别,它都会在有和没有当前单词的情况下递归,当它到达所有单词的末尾时打印结果:

      #include <iostream>
      #include <string>
      #include <vector>
      
      void recurse(std::vector<std::string> &values,size_t level,std::string str) {
        if (level<values.size()) {
          recurse(values,level+1,str+values[level]);
          recurse(values,level+1,str);
        } else {
          std::cout<<str<<"\n";
        }
      }
      
      int main(int argc, char*argv[]) {
        if (argc<2)
          std::cout<<argv[0]<<" <word> [<word> [...]]\n";
        else {
          std::vector<std::string> values;
          for(int i=1;i<argc;++i) {
            values.push_back(argv[i]);
          }
          recurse(values,0,"");
        }
        return 0;
      }
      

      当使用./a.out Hello New World 运行时会产生:

      HelloNewWorld
      HelloNew
      HelloWorld
      Hello
      NewWorld
      New
      World
      

      【讨论】:

      • 如果您将按值获取向量替换为按引用获取向量,则此解决方案非常有效(且有效)。
      • 事实上,对于这个专门的用例,虽然它具有相同的时间复杂度(我相信),但它应该具有更小的系数,快速基准测试似乎在我的编译器上证实了这一点。
      • @AlexanderHuszagh 当我编写代码时,问题说接受一个字符串并进行字符组合,因此函数接受一个字符串。然后问题发生了变化,我摆弄了我的解决方案。但我错过了 - 感谢您的提醒。
      • @JerryJeremiah 这太棒了!它运行得非常高效,虽然它看起来不像是尾递归,但你是否使用其他一些高级方法来实现这样的性能?
      • @AndrewLeFevre 尾递归并不一定会使其更快 - 它只是确保您不会使用所有堆栈空间。 C++ 中的尾递归优化是不可预测的,通常不能指望。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2020-08-10
      • 2013-02-01
      • 1970-01-01
      • 2021-05-03
      • 2021-04-02
      • 2013-07-20
      • 1970-01-01
      相关资源
      最近更新 更多