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 ループが終了すると、プログラムは前のフレームに戻ります。つまり、バックトラックです。

私は常に再帰に問題を抱えていましたが、今ではそれをバックトラッキングと組み合わせて、可能なすべての組み合わせを生成する必要があります。私が間違っていることの指針はありますか?私は何をすべきか、または見落としていますか?

PS: これは、順序付けられた 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();

    }
  }
}

丁寧な回答をありがとう、ジェイソン!

4

3 に答える 3

18

N 個の組み合わせを形成することを考える良い方法は、組み合わせの木のような構造を見ることです。そのツリーをたどることは、実装したいアルゴリズムの再帰的な性質と、再帰プロセスがどのように機能するかについて考える自然な方法になります。

たとえば、シーケンス があり{1, 2, 3, 4}、そのセット内の 3 つの組み合わせをすべて見つけたいとします。組み合わせの「ツリー」は次のようになります。

                              root
                        ________|___
                       |            | 
                     __1_____       2
                    |        |      |
                  __2__      3      3
                 |     |     |      |
                 3     4     4      4

pre-order traversal を使用してルートからトラバースし、リーフ ノードに到達したときに組み合わせを識別すると、組み合わせが得られます。

{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 つがどのように連携するか。

于 2012-03-04T06:02:04.087 に答える
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;
    }
于 2012-03-04T07:10:58.477 に答える
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 
于 2016-02-28T22:31:44.417 に答える