web-dev-qa-db-ja.com

C ++でn個のアイテムの可能なすべてのkの組み合わせを作成する

1からnに番号が付けられたn人がいます。これらのkからn人のすべての異なる組み合わせを生成して出力するコードを作成する必要があります。そのために使用されるアルゴリズムを説明してください。

35
Prannoy Mittal

組み合わせの意味での組み合わせについて質問していると仮定します(つまり、要素の順序は重要ではないため、[1 2 3][2 1 3]と同じです)。誘導/再帰を理解している場合、アイデアは非常に簡単です:すべてのK- elementの組み合わせを取得するには、最初に既存の人々のセットから組み合わせの初期要素を選択し、この初期値を「連結」します初期要素に続く要素から生成された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 ] 
54
dorserg

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

output

 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

分析とアイデア

全体のポイントは、数値のバイナリ表現で遊ぶことです。たとえば、バイナリの数値70111

したがって、このバイナリ表現は、割り当てリストのように見ることもできます:

各ビットiがビットが設定されている場合(つまり1)は、i番目のアイテムが割り当てられます。

次に、単純に連続する2進数のリストを計算し、2進表現(非常に高速)を活用することにより、[〜#〜] n [〜#〜]のすべての組み合わせを計算するアルゴリズムを提供します。 overk.

(一部の実装の)最後のsortingnot requiredです。これは、結果を決定論的に正規化する方法です。つまり、同じ数(N、K)と同じアルゴリズムに対して同じorderの組み合わせが返されます

数値表現とそれらの組み合わせ、順列、べき集合(およびその他の興味深いもの)との関係について詳しく読むには、 Combinatorial number systemFactorial number system をご覧ください

39
Nikos M.

セットの数が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が必要で、現在の「コンボ」が010110であると仮定します。明らかに、次のコンボは011001でなければなりません。011001は:010000 | 001000 | 000001

010000:LSB側の1を継続的に削除しました。 001000:LSB側の連続1の次に1を設定します。 000001:LSBの1を右に連続的にシフトし、LSBビットを削除します。

int x = combo & -combo;

これにより、最も低い1が取得されます。

int y = combo + x;

これにより、LSB側の1が連続して削除され、その次が1に設定されます(上記の場合、010000 | 001000)

int z = (combo & ~y)

これにより、LSB側(000110)の連続した1が得られます。

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

これは、「LSBの1を右に連続的にシフトし、LSBビットを削除する」ためです。

したがって、最終的な仕事は、上記のOR yです。

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 
8
user7781254

Pythonでは、これはitertools.combinationsとして実装されます

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

C++では、このような組み合わせ関数は置換関数に基づいて実装できます。

基本的な考え方は、サイズnのベクトルを使用し、内部でk個のアイテムのみを1に設定することです。nchoosekのすべての組み合わせは、各順列でk個のアイテムを収集することで取得できます。組み合わせは通常非常に大きな数であるため、大きなスペースを必要とする最も効率的な方法ではないかもしれませんが。ジェネレーターとして実装するか、作業コードを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 

このソリューションは、実際には c ++での組み合わせの生成 の複製です

7
Ning

私は、C#でクラスを作成して、二項係数を操作するための一般的な関数を処理しました。これは、問題が発生する問題のタイプです。次のタスクを実行します。

  1. N個のKをファイルに出力するために、すべてのKインデックスをNice形式で出力します。 Kインデックスは、より説明的な文字列または文字で置き換えることができます。この方法により、この種の問題の解決は非常に簡単になります。

  2. Kインデックスを、ソートされた二項係数テーブルのエントリの適切なインデックスに変換します。この手法は、反復に依存する古い公開手法よりもはるかに高速です。これは、Pascalの三角形に固有の数学プロパティを使用して行われます。私の論文はこれについて語っています。私はこのテクニックを最初に発見して公開したと思います。

  3. ソートされた二項係数テーブルのインデックスを、対応するKインデックスに変換します。他のソリューションよりも高速だと思います。

  4. Mark Dominus メソッドを使用して二項係数を計算します。これはオーバーフローする可能性がはるかに低く、より大きな数値で機能します。

  5. このクラスは.NET C#で記述されており、一般的なリストを使用して、問題に関連するオブジェクト(ある場合)を管理する方法を提供します。このクラスのコンストラクターは、InitTableというbool値を取ります。この値は、trueの場合、管理対象のオブジェクトを保持する汎用リストを作成します。この値がfalseの場合、テーブルは作成されません。上記の4つの方法を実行するために、テーブルを作成する必要はありません。テーブルにアクセスするためのアクセサメソッドが提供されています。

  6. クラスとそのメソッドの使用方法を示す関連テストクラスがあります。 2つのケースで広範囲にテストされており、既知のバグはありません。

このクラスについて読み、コードをダウンロードするには、 Tabinizing 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ケースを選択するたびに異なるファイルを使用します。

2
Bob Bryan

これが、この問題を解決するために思いついたアルゴリズムです。コードで動作するように変更できる必要があります。

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

どのように動作するかの説明を見ることができます こちら

2
android927

また、訪問済みの配列を維持することにより、バックトラッキングを使用して実行できます。

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;

}
0
Arpit Gupta

以下のリンクの背後には、この問題に対する一般的なC#の回答があります。オブジェクトのリストからすべての組み合わせをフォーマットする方法。結果をkの長さにのみ簡単に制限できます。

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

0
jaolho