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

Posted

技术标签:

【中文标题】使用递归和回溯生成所有可能的组合【英文标题】:Using recursion and backtracking to generate all possible combinations 【发布时间】:2012-03-22 01:51:52 【问题描述】:

我正在尝试实现一个类,该类将在给定元素数量和组合大小的情况下生成所有可能的无序 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 元组做同样的事情。

提前致谢!

/////////////////////////////////////// /////////////////////////////////////////////p>

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

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 的详尽回复!

【问题讨论】:

您可以将它们作为参数传递给递归函数,而不是将到目前为止的结果存储在成员变量中。这可能有助于弄清楚逻辑。 作业要求我存储它们,但也许它会帮助我更好地理解它。谢谢你的建议,我试试看。 另外,pos 只是你的tuple 向量的大小,所以你应该改用tuple.size() @howardh ,您使用tuple.size() 是对的,感谢您指出这一点 【参考方案1】:

考虑形成 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 值告诉我们在集合中的哪个位置开始向tupletuple 添加值,一旦我们'重新完整,将是集合中组合的一个实例。

然后,您需要从另一个非递归函数调用 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

请注意,这不是算法的最快版本,因为我们采用了一些我们不需要的额外分支,这会创建一些不必要的复制和函数调用等。.. . 但希望它能理解递归和回溯的一般概念,以及两者如何协同工作。

【讨论】:

非常感谢!正是我正在寻找的,很好的解释和彻底的。你的代码和我的很相似,所以我立刻明白我做错了什么。【参考方案2】:

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

将您的一组节点表示为一组位。如果您需要 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 @Loki Astari:+1 有趣的解决方案。顺便说一句,快速的问题......你提到这个方法是迭代的,但似乎 shiftBits 被递归调用,所以最后这仍然是一个递归算法,对吧? @Jason:它的递归方式与加法递归相同。 9999 + 1. => 9+1 = 0 加一所以现在我必须添加(十位)1 + 9 = 0 加一所以现在我添加(以百计)1 + 9 = 0 加一所以现在我将(以千计)1 + 9 = 0 加一,所以现在我有(以 10K 计)1。它可能在内部使用递归来实现移位操作,但 I would not call it a recursive solution 因为它不会在结果集中递归。就像在这里的加法示例中一样,我在 4 位数字上进行递归,而不是在所有 10,000 个元素的结果集中(所以我不会称加法递归)。【参考方案3】:

在 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 

【讨论】:

以上是关于使用递归和回溯生成所有可能的组合的主要内容,如果未能解决你的问题,请参考以下文章

LeetCode || 递归 / 回溯

使用递归和回溯查找所有可能的多米诺骨牌链[关闭]

[leetcode 40. 组合总和 II] 不排序使用哈希表+dfs递归 vs 排序栈+回溯

递归与回溯6:LeetCode39组合总和(可重复使用)

递归与回溯9:子集问题-求子集

Hot10039. 组合总和