使用递归和回溯来生成所有可能的组合

10

我正在尝试实现一个类,该类将生成所有可能的无序n元组或组合,给定元素数量和组合大小。

换句话说,在调用以下内容时:

NTupleUnordered unordered_tuple_generator(3, 5, print);
unordered_tuple_generator.Start();

print() 是在构造函数中设置的回调函数。 输出应该是:

{0,1,2}
{0,1,3}
{0,1,4}
{0,2,3}
{0,2,4}
{0,3,4}
{1,2,3}
{1,2,4}
{1,3,4}
{2,3,4}

到目前为止,这是我所拥有的内容:

class NTupleUnordered {
public:
    NTupleUnordered( int k, int n, void (*cb)(std::vector<int> const&) );
    void Start();
private:
    int tuple_size;                            //how many
    int set_size;                              //out of how many
    void (*callback)(std::vector<int> const&); //who to call when next tuple is ready
    std::vector<int> tuple;                    //tuple is constructed here
    void add_element(int pos);                 //recursively calls self
};

这是递归函数的实现,Start()只是一个启动函数,以便拥有更清晰的界面,它仅调用add_element(0)函数。

void NTupleUnordered::add_element( int pos )
{

  // base case
  if(pos == tuple_size)
  {
      callback(tuple);   // prints the current combination
      tuple.pop_back();  // not really sure about this line
      return;
  }

  for (int i = pos; i < set_size; ++i)
  {
    // if the item was not found in the current combination
    if( std::find(tuple.begin(), tuple.end(), i) == tuple.end())
    {
      // add element to the current combination
      tuple.push_back(i);
      add_element(pos+1); // next call will loop from pos+1 to set_size and so on

    }
  }
}

如果我想生成所有固定大小为N的可能组合,比如大小为3的组合,可以这样做:

for (int i1 = 0; i1 < 5; ++i1) 
{
  for (int i2 = i1+1; i2 < 5; ++i2) 
  {
    for (int i3 = i2+1; i3 < 5; ++i3) 
    {
        std::cout << "{" << i1 << "," << i2 << "," << i3 << "}\n";
    }
  }
}
如果N不是一个常量,你需要一个递归函数来模拟上述函数,通过在它自己的框架中执行每个for循环。当for循环终止时,程序返回到先前的框架,换句话说,回溯。

我总是遇到递归问题,现在我需要将其与回溯相结合以生成所有可能的组合。有什么错误之处吗?我应该做什么或者我忽略了什么?

P.S:这是一个大学作业,还包括为有序n元组做基本相同的事情。

提前感谢!

/////////////////////////////////////////////////////////////////////////////////////////

只是想跟进正确的代码,以防其他人也想知道同样的事情。

void NTupleUnordered::add_element( int pos)
{

  if(static_cast<int>(tuple.size()) == tuple_size)
  {
    callback(tuple);
    return;
  }

  for (int i = pos; i < set_size; ++i)
  {
        // add element to the current combination
        tuple.push_back(i);
        add_element(i+1); 
        tuple.pop_back();     
  }
}

对于有序n元组的情况:

void NTupleOrdered::add_element( int pos )
{
  if(static_cast<int>(tuple.size()) == tuple_size)
  {
    callback(tuple);
    return;
  }

  for (int i = pos; i < set_size; ++i)
  {
    // if the item was not found in the current combination
    if( std::find(tuple.begin(), tuple.end(), i) == tuple.end())
    {
        // add element to the current combination
        tuple.push_back(i);
        add_element(pos);
        tuple.pop_back();

    }
  }
}

感谢Jason提供详尽的回答!


与其将迄今为止的结果存储在成员变量中,不如将它们作为递归函数的参数传递。这样做可能有助于弄清逻辑。 - howard
这个任务要求我存储它们,但或许这会帮助我更好地理解它。谢谢你的建议,我会试一下。 - Lechuzza
另外,pos 只是您的 tuple 向量的大小,因此您应该使用 tuple.size() - howard
@howardh,你说得对,使用 tuple.size() 是正确的,感谢指出。 - Lechuzza
3个回答

18
一种很好的思考生成N个组合的方式是将其结构看作一个组合树。遍历这棵树成为自然地思考你希望实现的递归算法以及递归过程如何工作的一种方式。
比如我们有一个序列{1, 2, 3, 4},并且我们想要在该集合中找到所有的3个元素的组合。那么"组合树"看起来像下面这样:
                              root
                        ________|___
                       |            | 
                     __1_____       2
                    |        |      |
                  __2__      3      3
                 |     |     |      |
                 3     4     4      4

通过使用先序遍历从根开始遍历,并在到达叶节点时识别组合,我们得到以下组合:

{1, 2, 3}
{1, 2, 4}
{1, 3, 4}
{2, 3, 4}

基本上的想法是使用索引值遍历数组,对于我们递归的每个阶段(在此情况下将是树的“层级”),增加到数组中以获取将包含在组合集中的值。还要注意,我们只需要递归N次。因此,您将拥有某个递归函数,其签名看起来类似于以下内容:

void recursive_comb(int step_val, int array_index, std::vector<int> tuple);

step_val表示我们需要递归的深度,array_index值告诉我们在集合中的位置从哪里开始添加值到tuple中,一旦完成,tuple将是集合中一个组合的实例。

然后,您需要从另一个非递归函数调用recursive_comb,该函数基本上通过初始化tuple向量并输入最大递归步骤(即我们希望在元组中有多少个值)来“启动”递归过程:

void init_combinations()
{
    std::vector<int> tuple;
    tuple.reserve(tuple_size); //avoids needless allocations
    recursive_comb(tuple_size, 0, tuple);
}

最后,你的recusive_comb函数应该如下所示:

void recursive_comb(int step_val, int array_index, std::vector<int> tuple)
{
    if (step_val == 0)
    {
        all_combinations.push_back(tuple); //<==We have the final combination
        return;
    }

    for (int i = array_index; i < set.size(); i++)
    {
        tuple.push_back(set[i]);
        recursive_comb(step_val - 1, i + 1, tuple); //<== Recursive step
        tuple.pop_back(); //<== The "backtrack" step
    }

    return;
}

你可以在这里看到这段代码的一个工作示例:http://ideone.com/78jkV

请注意,这不是算法的最快版本,因为我们进行了一些不必要的分支,导致了一些不必要的复制和函数调用等... 但希望它能够传达递归和回溯的一般思想以及它们如何协同工作。


1
非常感谢!正是我一直在寻找的,讲解详细、深入浅出。你的代码与我的非常相似,所以我马上明白了我的错误所在。 - Lechuzza

3

个人而言,我会选择一个简单的迭代解决方案。

将节点集表示为一组位。如果需要5个节点,则有5位,每位表示特定的节点。如果您想要其中3个作为元组,则只需设置3位并跟踪它们的位置。

基本上,这是对查找所有不同节点组合子集的简单变化。经典实现将节点集表示为整数。整数中的每个位表示一个节点。空集是0。然后,您只需递增整数,每个新值都是新的节点集(位模式表示节点集)。在这个变化中,您只需确保始终有3个节点处于活动状态即可。

只需帮助我思考,我从3个顶部节点开始 {4,3,2}。然后我倒数计数。但修改以反向计数也很容易。

#include <boost/dynamic_bitset.hpp>
#include <iostream>


class TuppleSet
{
    friend std::ostream& operator<<(std::ostream& stream, TuppleSet const& data);

    boost::dynamic_bitset<> data;    // represents all the different nodes
    std::vector<int>        bitpos;  // tracks the 'n' active nodes in the tupple

    public:
        TuppleSet(int nodes, int activeNodes)
            : data(nodes)
            , bitpos(activeNodes)
        {
            // Set up the active nodes as the top 'activeNodes' node positions.
            for(int loop = 0;loop < activeNodes;++loop)
            {
                bitpos[loop]        = nodes-1-loop;
                data[bitpos[loop]]  = 1;
            }
        }
        bool next()
        {
            // Move to the next combination
            int bottom  = shiftBits(bitpos.size()-1, 0);
            // If it worked return true (otherwise false)
            return bottom >= 0;
        }
    private:
        // index is the bit we are moving. (index into bitpos)
        // clearance is the number of bits below it we need to compensate for.
        //
        //  [ 0, 1, 1, 1, 0 ]   =>    { 3, 2, 1 }
        //             ^
        //             The bottom bit is move down 1 (index => 2, clearance => 0)
        //  [ 0, 1, 1, 0, 1]    =>    { 3, 2, 0 }
        //                ^
        //             The bottom bit is moved down 1 (index => 2, clearance => 0)
        //             This falls of the end
        //          ^
        //             So we move the next bit down one (index => 1, clearance => 1)
        //  [ 0, 1, 0, 1, 1]
        //                ^
        //             The bottom bit is moved down 1 (index => 2, clearance => 0)
        //             This falls of the end
        //             ^
        //             So we move the next bit down one (index =>1, clearance => 1)
        //             This does not have enough clearance to move down (as the bottom bit would fall off)
        //      ^      So we move the next bit down one (index => 0, clearance => 2)
        // [ 0, 0, 1, 1, 1] 
        int shiftBits(int index, int clerance)
        {
            if (index == -1)
            {   return -1;
            }
            if (bitpos[index] > clerance)
            {
                --bitpos[index];
            }
            else
            {
                int nextBit = shiftBits(index-1, clerance+1);
                bitpos[index] = nextBit-1;
            }
            return bitpos[index];
        }
};

std::ostream& operator<<(std::ostream& stream, TuppleSet const& data)
{
    stream << "{ ";
    std::vector<int>::const_iterator loop = data.bitpos.begin();
    if (loop != data.bitpos.end())
    {
        stream << *loop;
        ++loop;
        for(; loop != data.bitpos.end(); ++loop)
        {
            stream << ", " << *loop;
        }
    }
    stream << " }";
    return stream;
}

主函数很简单:

int main()
{
    TuppleSet   s(5,3);

    do
    {
        std::cout << s << "\n";
    }
    while(s.next());
}

输出结果为:
{ 4, 3, 2 }
{ 4, 3, 1 }
{ 4, 3, 0 }
{ 4, 2, 1 }
{ 4, 2, 0 }
{ 4, 1, 0 }
{ 3, 2, 1 }
{ 3, 2, 0 }
{ 3, 1, 0 }
{ 2, 1, 0 }

使用循环的shiftBits()版本
    int shiftBits()
    {
        int bottom   = -1;
        for(int loop = 0;loop < bitpos.size();++loop)
        {
            int index   = bitpos.size() - 1 - loop;
            if (bitpos[index] > loop)
            {
                bottom = --bitpos[index];
                for(int shuffle = loop-1; shuffle >= 0; --shuffle)
                {
                    int index   = bitpos.size() - 1 - shuffle;
                    bottom = bitpos[index] = bitpos[index-1]  - 1;
                }
                break;
            }
        }
        return bottom;
    }

我一直在寻找使用递归和回溯的解决方案,但是你的解决方案很有趣,感谢你的回复!+1 - Lechuzza
@Loki Astari:有趣的解决方案,点赞。顺便问一下...你提到这种方法是迭代的,但似乎shiftBits被递归调用,所以最终这仍然是一个递归算法,对吗? - Jason
@Jason:它的递归方式与加法的递归方式相同。9999 + 1. => 9+1 = 0,有一个进位,现在我必须加上(十位)1 + 9 = 0,有一个进位,现在我加上(百位)1 + 9 = 0,有一个进位,现在我加上(千位)1 + 9 = 0,有一个进位,现在我有了(万位)1。它可能在内部使用递归来实现移位操作,但是“我不会称其为递归解决方案”,因为它没有在结果集上进行递归。就像在这里的加法示例中,我正在跨越4个数字进行递归,而不是跨越所有10,000个元素的结果集(因此我不会称加法为递归)。 - Martin York

0

在MATLAB中:

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% combinations.m

function combinations(n, k, func)
assert(n >= k);
n_set = [1:n];
k_set = zeros(k, 1);
recursive_comb(k, 1, n_set, k_set, func)
return

function recursive_comb(k_set_index, n_set_index, n_set, k_set, func)
if k_set_index == 0,
  func(k_set);
  return;
end;
for i = n_set_index:length(n_set)-k_set_index+1,
  k_set(k_set_index) = n_set(i);
  recursive_comb(k_set_index - 1, i + 1, n_set, k_set, func); 
end;
return;

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

Test:
>> combinations(5, 3, @(x) printf('%s\n', sprintf('%d ', x)));
3 2 1 
4 2 1 
5 2 1 
4 3 1 
5 3 1 
5 4 1 
4 3 2 
5 3 2 
5 4 2 
5 4 3 

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接