在 C++ 中用递归替换循环的 N 级

Replacing N-level for loops with recursion in C++

本文关键字:循环 替换 递归 C++      更新时间:2023-10-16

一段时间以来,我一直在尝试想出一种方法来计算单词字符串的所有各种组合。但是,与网络上的大多数组合方法不同,该算法必须生成每个组合,包括所有组合元素不在单个组合中的组合。即,如果我将"你好"、"新"和"世界"组合在一起,我正在寻找的组合是:

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;
}

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

在每个级别上,当它到达所有单词的末尾时,它会递归,无论是否使用当前单词打印结果:

#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

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

#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(前者不包括在内,但在这里建议(。我强烈建议阅读霍华德·欣南特(Howard Hinnant(的博客文章:它很有启发性。他的实现的时间复杂度和蛮力速度超过了大多数其他建议。如果您需要高性能组合或排列,他已经为您完成了工作。

如果我理解正确,您希望生成字符串的所有组合。 在这种情况下,您可以使用 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和队列中。当您弹出ACDnext 时,您会生成CDADAC但现在您不会将CD添加到队列中,因为它在集合中。

编辑

我看到了你的问题,我的答案适用于字符串,但你可以在vector<string>上使用相同的原理来生成所有组合ABCD这只会Hello(A)World(B)...

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

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