在C ++中创建n个项目的所有可能k组合

时间:2012-10-20 19:12:05

标签: c++ algorithm math combinations combinatorics

有{n}个人从1n。我必须编写一个代码,用于生成和打印来自这些k的{​​{1}}人的所有不同组合。请解释用于此的算法。

12 个答案:

答案 0 :(得分:53)

我假设您在组合意义上询问组合(即元素的顺序无关紧要,因此[1 2 3][2 1 3]相同)。这个想法非常简单,如果你理解归纳/递归:要获得所有K - 元素组合,你首先从现有的一组人中选择一个组合的初始元素,然后你“连接”这个初始元素包含K-1个人的所有可能组合,这些人来自成功初始元素的元素。

举个例子,假设我们想要从一组5人中抽取3个人的所有组合。然后,所有可能的3人组合可以用2人的所有可能组合来表达:

comb({ 1 2 3 4 5 }, 3) =
{ 1, comb({ 2 3 4 5 }, 2) } and
{ 2, comb({ 3 4 5 }, 2) } and
{ 3, comb({ 4 5 }, 2) }

这是实现这个想法的C ++代码:

#include <iostream>
#include <vector>

using namespace std;

vector<int> people;
vector<int> combination;

void pretty_print(const vector<int>& v) {
  static int count = 0;
  cout << "combination no " << (++count) << ": [ ";
  for (int i = 0; i < v.size(); ++i) { cout << v[i] << " "; }
  cout << "] " << endl;
}

void go(int offset, int k) {
  if (k == 0) {
    pretty_print(combination);
    return;
  }
  for (int i = offset; i <= people.size() - k; ++i) {
    combination.push_back(people[i]);
    go(i+1, k-1);
    combination.pop_back();
  }
}

int main() {
  int n = 5, k = 3;

  for (int i = 0; i < n; ++i) { people.push_back(i+1); }
  go(0, k);

  return 0;
}

这是N = 5, K = 3的输出:

combination no 1:  [ 1 2 3 ] 
combination no 2:  [ 1 2 4 ] 
combination no 3:  [ 1 2 5 ] 
combination no 4:  [ 1 3 4 ] 
combination no 5:  [ 1 3 5 ] 
combination no 6:  [ 1 4 5 ] 
combination no 7:  [ 2 3 4 ] 
combination no 8:  [ 2 3 5 ] 
combination no 9:  [ 2 4 5 ] 
combination no 10: [ 3 4 5 ] 

答案 1 :(得分:37)

来自Rosetta code

#include <algorithm>
#include <iostream>
#include <string>

void comb(int N, int K)
{
    std::string bitmask(K, 1); // K leading 1's
    bitmask.resize(N, 0); // N-K trailing 0's

    // print integers and permute bitmask
    do {
        for (int i = 0; i < N; ++i) // [0..N-1] integers
        {
            if (bitmask[i]) std::cout << " " << i;
        }
        std::cout << std::endl;
    } while (std::prev_permutation(bitmask.begin(), bitmask.end()));
}

int main()
{
    comb(5, 3);
}

<强>输出

 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

分析和想法

重点是使用数字的二进制表示 例如,二进制数 7 0111

因此,这个二进制表示也可以看作赋值列表

对于每个位 i 如果该位置位(即 1 )意味着 i 项目被分配,否则不会。

然后通过简单地计算连续二进制数列表并利用二进制表示(可能非常快)给出一个算法来计算 N 超过 k 的所有组合

最后(某些实施)的排序 不需要。这只是一种确定性地归一化结果的方法,即对于相同的数字(N,K)和相同的算法,返回相同的 order 组合

有关数字表示及其与组合,排列,权力集(以及其他有趣内容)的关系的进一步阅读,请查看Combinatorial number systemFactorial number system

答案 2 :(得分:8)

如果集合的编号在32,64或机器本机原始大小之内,那么您可以通过简单的位操作来完成。

template<typename T>
void combo(const T& c, int k)
{
    int n = c.size();
    int combo = (1 << k) - 1;       // k bit sets
    while (combo < 1<<n) {

        pretty_print(c, combo);

        int x = combo & -combo;
        int y = combo + x;
        int z = (combo & ~y);
        combo = z / x;
        combo >>= 1;
        combo |= y;
    }
}

此示例按字典顺序调用pretty_print()函数。

例如。你想拥有6C3并假设当前的组合&#39;是010110。 显然下一个组合必须是011001。 011001是:     010000 | 001000 | 000001

010000:连续删除1个LSB侧。 001000:在LSB侧连续1的下一个上设置1。 000001:向右移动1个LSB并移除LSB位。

int x = combo & -combo;

获得最低1。

int y = combo + x;

这将连续消除1个LSB侧并在下一个上设置1(在上面的例子中,010000 | 001000)

int z = (combo & ~y)

这为你提供了连续1的LSB侧(000110)。

combo = z / x;
combo >> =1;

这是为了将LSB的1s连续向右移动并移除LSB位&#39;。

所以最后的工作是与以上相同。

combo |= y;

一些简单的具体例子:

#include <bits/stdc++.h>

using namespace std;

template<typename T>
void pretty_print(const T& c, int combo)
{
    int n = c.size();
    for (int i = 0; i < n; ++i) {
        if ((combo >> i) & 1)
            cout << c[i] << ' ';
    }
    cout << endl;
}

template<typename T>
void combo(const T& c, int k)
{
    int n = c.size();
    int combo = (1 << k) - 1;       // k bit sets
    while (combo < 1<<n) {

        pretty_print(c, combo);

        int x = combo & -combo;
        int y = combo + x;
        int z = (combo & ~y);
        combo = z / x;
        combo >>= 1;
        combo |= y;
    }
}

int main()
{
    vector<char> c0 = {'1', '2', '3', '4', '5'};
    combo(c0, 3);

    vector<char> c1 = {'a', 'b', 'c', 'd', 'e', 'f', 'g'};
    combo(c1, 4);
    return 0;
}

结果:

1 2 3 
1 2 4 
1 3 4 
2 3 4 
1 2 5 
1 3 5 
2 3 5 
1 4 5 
2 4 5 
3 4 5 
a b c d 
a b c e 
a b d e 
a c d e 
b c d e 
a b c f 
a b d f 
a c d f 
b c d f 
a b e f 
a c e f 
b c e f 
a d e f 
b d e f 
c d e f 
a b c g 
a b d g 
a c d g 
b c d g 
a b e g 
a c e g 
b c e g 
a d e g 
b d e g 
c d e g 
a b f g 
a c f g 
b c f g 
a d f g 
b d f g 
c d f g 
a e f g 
b e f g 
c e f g 
d e f g 

答案 3 :(得分:7)

在Python中,这是作为itertools.combinations

实现的

https://docs.python.org/2/library/itertools.html#itertools.combinations

在C ++中,可以基于置换函数实现这种组合函数。

基本思想是使用大小为n的向量,并且仅将k项设置为1,然后通过收集每个排列中的k个项来获得nchoosek的所有组合。 虽然它可能不是需要大空间的最有效方式,但组合通常是非常大的数量。最好将其作为生成器实现,或将工作代码放入do_sth()。

代码示例:

#include <vector>
#include <iostream>
#include <iterator>
#include <algorithm>

using namespace std;

int main(void) {

  int n=5, k=3;

  // vector<vector<int> > combinations;
 vector<int> selected;
 vector<int> selector(n);
 fill(selector.begin(), selector.begin() + k, 1);
 do {
     for (int i = 0; i < n; i++) {
      if (selector[i]) {
            selected.push_back(i);
      }
     }
     //     combinations.push_back(selected);
         do_sth(selected);
     copy(selected.begin(), selected.end(), ostream_iterator<int>(cout, " "));
     cout << endl;
     selected.clear();
 }
 while (prev_permutation(selector.begin(), selector.end()));

  return 0;
}

,输出

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 

此解决方案实际上是重复的 Generating combinations in c++

答案 4 :(得分:2)

我在C#中编写了一个类来处理使用二项式系数的常用函数,这是你的问题所处的问题类型。它执行以下任务:

  1. 以任意N选择K到文件的格式输出所有K索引。 K索引可以用更具描述性的字符串或字母代替。这种方法使解决这类问题变得非常简单。

  2. 将K索引转换为已排序二项系数表中条目的正确索引。这种技术比依赖迭代的旧发布技术快得多。它通过使用Pascal三角形中固有的数学属性来实现。我的论文谈到了这一点。我相信我是第一个发现和发布这种技术的人。

  3. 将已排序的二项系数表中的索引转换为相应的K索引。我相信它也比其他解决方案更快。

  4. 使用Mark Dominus方法计算二项式系数,这样就不太可能溢出并使用更大的数字。

  5. 该类是用.NET C#编写的,它提供了一种通过使用通用列表来管理与问题相关的对象(如果有)的方法。此类的构造函数采用名为InitTable的bool值,当为true时,将创建一个通用列表来保存要管理的对象。如果此值为false,则不会创建表。不需要创建表来执行上述4种方法。提供访问者方法来访问该表。

  6. 有一个关联的测试类,它显示了如何使用该类及其方法。它已经过2个案例的广泛测试,并且没有已知的错误。

  7. 要阅读此课程并下载代码,请参阅Tablizing The Binomial Coeffieicent

    将类移植到C ++应该非常简单。

    您的问题的解决方案涉及为每个N选择K案例生成K索引。例如:

    int NumPeople = 10;
    int N = TotalColumns;
    // Loop thru all the possible groups of combinations.
    for (int K = N - 1; K < N; K++)
    {
       // Create the bin coeff object required to get all
       // the combos for this N choose K combination.
       BinCoeff<int> BC = new BinCoeff<int>(N, K, false);
       int NumCombos = BinCoeff<int>.GetBinCoeff(N, K);
       int[] KIndexes = new int[K];
       BC.OutputKIndexes(FileName, DispChars, "", " ", 60, false);
       // Loop thru all the combinations for this N choose K case.
       for (int Combo = 0; Combo < NumCombos; Combo++)
       {
          // Get the k-indexes for this combination, which in this case
          // are the indexes to each person in the problem set.
          BC.GetKIndexes(Loop, KIndexes);
          // Do whatever processing that needs to be done with the indicies in KIndexes.
          ...
       }
    }
    

    OutputKIndexes方法也可以用于将K索引输出到文件,但是对于每个N选择K的情况,它将使用不同的文件。

答案 5 :(得分:2)

这是我提出的用于解决此问题的算法。您应该能够修改它以使用您的代码。

void r_nCr(const unsigned int &startNum, const unsigned int &bitVal, const unsigned int &testNum) // Should be called with arguments (2^r)-1, 2^(r-1), 2^(n-1)
{
    unsigned int n = (startNum - bitVal) << 1;
    n += bitVal ? 1 : 0;

    for (unsigned int i = log2(testNum) + 1; i > 0; i--) // Prints combination as a series of 1s and 0s
        cout << (n >> (i - 1) & 1);
    cout << endl;

    if (!(n & testNum) && n != startNum)
        r_nCr(n, bitVal, testNum);

    if (bitVal && bitVal < testNum)
        r_nCr(startNum, bitVal >> 1, testNum);
}

您可以看到有关其工作原理的解释here

答案 6 :(得分:0)

以下链接的背后是此问题的通用C#答案:如何格式化对象列表中的所有组合。您可以非常轻松地将结果限制为k的长度。

https://stackoverflow.com/a/40417765/2613458

答案 7 :(得分:0)

也可以通过维护访问数组来使用回溯来完成。

void foo(vector<vector<int> > &s,vector<int> &data,int go,int k,vector<int> &vis,int tot)
{

    vis[go]=1;
    data.push_back(go);
    if(data.size()==k)
    {
        s.push_back(data);
        vis[go]=0;
    data.pop_back();
        return;
    }

    for(int i=go+1;i<=tot;++i)
    {
       if(!vis[i])
       {
           foo(s,data,i,k,vis,tot);
       }
    }
    vis[go]=0;
    data.pop_back();
}


vector<vector<int> > Solution::combine(int n, int k) {
   vector<int> data;
   vector<int> vis(n+1,0);
   vector<vector<int> > sol;
   for(int i=1;i<=n;++i)
   {
       for(int i=1;i<=n;++i) vis[i]=0;
   foo(sol,data,i,k,vis,n);
   }
   return sol;

}

答案 8 :(得分:0)

我认为我简单的“所有可能的组合生成器”可能会对某人有所帮助,我认为这是构建更大更好的东西的一个很好的例子

只需从字符串数组中删除/添加 ,您就可以将N (字符)更改为您喜欢的任何字符(您可以更改 当前字符数为36

您还可以添加更多循环更改K (生成的组合的大小),对于每个元素,必须有一个额外的循环。 当前大小为4

#include<iostream>

using namespace std;

int main() {
string num[] = {"0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z" };

for (int i1 = 0; i1 < sizeof(num)/sizeof(string); i1++) {
    for (int i2 = 0; i2 < sizeof(num)/sizeof(string); i2++) {
        for (int i3 = 0; i3 < sizeof(num)/sizeof(string); i3++) {
            for (int i4 = 0; i4 < sizeof(num)/sizeof(string); i4++) {
                cout << num[i1] << num[i2] << num[i3] << num[i4] << endl;
            }
        }
    }
}}

结果

0000
0001
0002
0003
0004
0005
0006
0007
0008
0009
000a
000b
000c
000d
000e
000f
000g
000h
000i
000j
000k
000l
000m
000n
000o
000p
000q
000r
000s
000t
000u
000v
000w
000x
000y
000z
0010
0011
0012
0013
0014
0015
0016
0017
0018
0019
001a
001b
...

请记住,组合的数量可能是可笑的。

答案 9 :(得分:0)

为使内容更完整,以下答案涵盖了数据集包含重复值的情况。该函数的编写风格与std :: next_permutation()十分接近,因此易于跟踪。

template< class RandomIt >
bool next_combination(RandomIt first, RandomIt n_first, RandomIt last)
{
  if (first == last || n_first == first || n_first == last)
  {
    return false;
  }

  RandomIt it_left = n_first;
  --it_left;
  RandomIt it_right = n_first;

  bool reset = false;
  while (true)
  {
    auto it = std::upper_bound(it_right, last, *it_left);

    if (it != last)
    {
      std::iter_swap(it_left, it);
      if (reset)
      {
        ++it_left;
        it_right = it;
        ++it_right;
        std::size_t left_len = std::distance(it_left, n_first);
        std::size_t right_len = std::distance(it_right, last);
        if (left_len < right_len)
        {
          std::swap_ranges(it_left, n_first, it_right);
          std::rotate(it_right, it_right+left_len, last);
        }
        else
        {
          std::swap_ranges(it_right, last, it_left);
          std::rotate(it_left, it_left+right_len, n_first);
        }
      }
      return true;
    }
    else
    {
      reset = true;
      if (it_left == first)
      {
        break;
      }
      --it_left;
      it_right = n_first;
    }
  }
  return false;
}

完整数据集表示在[first,last)范围内。当前组合表示在[first,n_first)范围内,范围[n_first,last)表示当前组合的补集。

由于组合与顺序无关,因此[first,n_first)和[n_first,last)保持升序以避免重复。

该算法的工作原理是,通过与大于A的右侧第一个值B交换来增加左侧的最后一个值A。交换之后,双方仍然有序。如果右侧没有这样的值B,那么我们开始考虑增加左侧的倒数第二个,直到左侧的所有值不小于右侧。

通过以下代码从集合中绘制2个元素的示例:

  std::vector<int> seq = {1, 1, 2, 2, 3, 4, 5};
  do
  {
    for (int x : seq)
    {
      std::cout << x << " ";
    }
    std::cout << "\n";
  } while (next_combination(seq.begin(), seq.begin()+2, seq.end()));

给予:

1 1 2 2 3 4 5 
1 2 1 2 3 4 5 
1 3 1 2 2 4 5 
1 4 1 2 2 3 5 
1 5 1 2 2 3 4 
2 2 1 1 3 4 5 
2 3 1 1 2 4 5 
2 4 1 1 2 3 5 
2 5 1 1 2 3 4 
3 4 1 1 2 2 5 
3 5 1 1 2 2 4 
4 5 1 1 2 2 3 

如果需要的话,检索前两个元素作为组合结果很简单。

答案 10 :(得分:0)

此解决方案的基本思想是模仿在高中时无需手动重复即可枚举所有组合的方式。假设com为长度为k的List [int],数字为给定n个项目的List [int],其中n> = k。 这个想法如下:

for x[0] in nums[0,...,n-1] 
    for x[1] in nums[idx_of_x[0] + 1,..,n-1]
        for x[2] in nums [idx_of_x[1] + 1,...,n-1]
        ..........
            for x[k-1] in nums [idx_of_x[k-2]+1, ..,n-1]

显然,k和n是变量参数,这使得不可能编写显式的多个嵌套的for循环。这是递归来解决问题的地方。 语句len(com) + len(nums[i:]) >= k检查其余未访问的项目前向列表是否可以提供项目。顺便说一句,我的意思是您不应该向后移动数字,以避免重复的组合,该组合由相同的一组项目组成,但顺序不同。换句话说,以这些不同的顺序,我们可以通过向前扫描列表来选择这些项目在列表中出现的顺序。更重要的是,此测试子句在内部修剪了递归树,使其仅包含n choose k递归调用。因此,运行时间为O({n choose k)。

from typing import List

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        assert 1 <= n <= 20
        assert 1 <= k <= n
        com_sets = []
        self._combine_recurse(k, list(range(1, n+1)), [], com_sets)
        return com_sets

    def _combine_recurse(self, k: int, nums: List[int], com: List[int], com_set: List[List[int]]):
        """
        O(C_n^k)
        """
        if len(com) < k:
            for i in range(len(nums)):
            # Once again, don't com.append() since com should not be global!
                if len(com) + len(nums[i:]) >= k:
                    self._combine_recurse(k, nums[i+1:], com + [nums[i]], com_set)
        else:
            if len(com) == k:
                com_set.append(com)
                print(com)
sol = Solution()
sol.combine(5, 3)

[1, 2, 3]
[1, 2, 4]
[1, 2, 5]
[1, 3, 4]
[1, 3, 5]
[1, 4, 5]
[2, 3, 4]
[2, 3, 5]
[2, 4, 5]
[3, 4, 5]

答案 11 :(得分:0)

这个模板化函数可以将任何类型的向量作为输入。
组合作为向量的向量返回。

/*
* Function return all possible combinations of k elements from N-size inputVector.
* The result is returned as a vector of k-long vectors containing all combinations.
*/
template<typename T> std::vector<std::vector<T>> getAllCombinations(const std::vector<T>& inputVector, int k)
{
    std::vector<std::vector<T>> combinations;
    std::vector<int> selector(inputVector.size());
    std::fill(selector.begin(), selector.begin() + k, 1);

    do {
        std::vector<int> selectedIds;
        std::vector<T> selectedVectorElements;
        for (int i = 0; i < inputVector.size(); i++) {
            if (selector[i]) {
                selectedIds.push_back(i);
            }
        }
        for (auto& id : selectedIds) {
            selectedVectorElements.push_back(inputVector[id]);
        }
        combinations.push_back(selectedVectorElements);
    } while (std::prev_permutation(selector.begin(), selector.end()));

    return combinations;
}