web-dev-qa-db-ja.com

順列の遅延生成

Clojureで遅延リストを作成できるような方法で、セットの順列を生成するアルゴリズムを探しています。つまり、私が要求するまで各置換が計算されず、すべての置換を一度にメモリに保存する必要がない置換のリストを反復処理したいと思います。

または、特定のセットが指定されたアルゴリズムを探しています。それは、そのセットの「次の」順列を返します。そのため、独自の出力で関数を繰り返し呼び出すと、元のセットのすべての順列が循環します。一部の順序(順序は何でも構いません)。

そのようなアルゴリズムはありますか?私が見たほとんどの順列生成アルゴリズムは、それらを一度に(通常は再帰的に)一度に生成する傾向があり、非常に大きなセットには拡張できません。 Clojure(または他の関数型言語)での実装は役に立ちますが、疑似コードからそれを理解できます。

84
Brian Carper

はい、あります「次の順列」アルゴリズムであり、それも非常に簡単です。 C++標準テンプレートライブラリ(STL)には、next_permutation

アルゴリズムは実際にnext順列を見つけます-辞書順で次の順列。アイデアはこれです:あなたが「32541」のようなシーケンスを与えられたとしましょう。次の順列は何ですか?

考えてみると「34125」です。そしてあなたの考えはおそらくこのようなものでした:「32541」では、

  • 「32」を固定して「541」の部分で後の置換を見つける方法はありません。その置換はすでに5、4、1の最後の置換であるため、降順でソートされます。
  • したがって、「2」をもっと大きなものに変更する必要があります-実際には、「541」部分のそれよりも大きい最小の数字、つまり4に変更します。
  • ここで、順列が「34」から始まることを決定すると、残りの数値は昇順になるはずなので、答えは「34125」になります。

アルゴリズムは、その推論の行を正確に実装することです。

  1. 降順で並べられている最も長い「尾」を見つけます。 (「541」部分。)
  2. 尾の直前の数( "2")を、尾の中でそれよりも大きい最小の数(4)に変更します。
  3. 尾を昇順に並べ替えます。

前の要素が現在の要素よりも小さくない限り、最後から始めて後ろに戻ることにより、(1。)を効率的に実行できます。 (2.)を実行するには、「4」を「2」と交換するだけで「34521」になります。これを実行すると、(3。)の並べ替えアルゴリズムを使用しないようにすることができます。以前と今も(これについて考えて)、降順で並べ替えられているため、逆にするだけで済みます。

C++コードはこれを正確に実行します(/usr/include/c++/4.0.0/bits/stl_algo.hシステム上、または この記事 );を参照してください。 C++イテレータに慣れていない場合は、[BidirectionalIterator]を[ポインタ]として読み取るのが簡単です。次の順列がない場合、つまり、すでに降順である場合、コードはfalseを返します。]

template <class BidirectionalIterator>
bool next_permutation(BidirectionalIterator first,
                      BidirectionalIterator last) {
    if (first == last) return false;
    BidirectionalIterator i = first;
    ++i;
    if (i == last) return false;
    i = last;
    --i;
    for(;;) {
        BidirectionalIterator ii = i--;
        if (*i <*ii) {
            BidirectionalIterator j = last;
            while (!(*i <*--j));
            iter_swap(i, j);
            reverse(ii, last);
            return true;
        }
        if (i == first) {
            reverse(first, last);
            return false;
        }
    }
}

O(n)順列ごとに時間がかかるように思えるかもしれませんが、より慎重に考えると、すべての順列に対して合計でO(n!)時間かかることを証明できますなので、O(1)-一定時間-順列ごとにのみです。

良い要素は、繰り返し要素のあるシーケンスがある場合でもアルゴリズムが機能することです。たとえば、「232254421」では、尾が「54421」として検出され、「2」と「4」が入れ替わります(つまり「232454221」 )、残りを逆にして、次の順列である "232412245"を与えます。

135
ShreevatsaR

並べ替えられる値の辞書式順序について話していると仮定すると、使用できる2つの一般的なアプローチがあります。

  1. 要素の1つの順列を次の順列に変換します(ShreevatsaRが投稿したとおり)、または
  2. n番目の順列を直接計算し、0から上にnをカウントします。

ネイティブとしてc ++を話さない人(私のように;-)の場合、アプローチ1は次の疑似コードから実装できます。リストなどの「演習として残された」;-):

1. scan the array from right-to-left (indices descending from N-1 to 0)
1.1. if the current element is less than its right-hand neighbor,
     call the current element the pivot,
     and stop scanning
1.2. if the left end is reached without finding a pivot,
     reverse the array and return
     (the permutation was the lexicographically last, so its time to start over)
2. scan the array from right-to-left again,
   to find the rightmost element larger than the pivot
   (call that one the successor)
3. swap the pivot and the successor
4. reverse the portion of the array to the right of where the pivot was found
5. return

以下は、CADBの現在の順列から始まる例です。

1. scanning from the right finds A as the pivot in position 1
2. scanning again finds B as the successor in position 3
3. swapping pivot and successor gives CBDA
4. reversing everything following position 1 (i.e. positions 2..3) gives CBAD
5. CBAD is the next permutation after CADB

2番目のアプローチ(nth順列の直接計算)では、N!N要素の順列。したがって、N要素を並べ替える場合、最初の(N-1)!順列は最小の要素から始まり、次の(N-1)!順列は2番目に小さいものから開始する必要があります。これは、次の再帰的なアプローチにつながります(ここでも、疑似コードでは、順列と位置に0から番号を付けています)。

To find permutation x of array A, where A has N elements:
0. if A has one element, return it
1. set p to ( x / (N-1)! ) mod N
2. the desired permutation will be A[p] followed by
   permutation ( x mod (N-1)! )
   of the elements remaining in A after position p is removed

したがって、たとえば、ABCDの13番目の順列は次のようになります。

perm 13 of ABCD: {p = (13 / 3!) mod 4 = (13 / 6) mod 4 = 2; ABCD[2] = C}
C followed by perm 1 of ABD {because 13 mod 3! = 13 mod 6 = 1}
  perm 1 of ABD: {p = (1 / 2!) mod 3 = (1 / 2) mod 2 = 0; ABD[0] = A}
  A followed by perm 1 of BD {because 1 mod 2! = 1 mod 2 = 1}
    perm 1 of BD: {p = (1 / 1!) mod 2 = (1 / 1) mod 2 = 1; BD[1] = D}
    D followed by perm 0 of B {because 1 mod 1! = 1 mod 1 = 0}
      B (because there's only one element)
    DB
  ADB
CADB

ちなみに、要素の「削除」は、ブール値の並列配列で表すことができます。これは、どの要素がまだ利用できるかを示しているため、再帰呼び出しごとに新しい配列を作成する必要はありません。

したがって、ABCDの順列を反復処理するには、0から23(4!-1)まで数え、対応する順列を直接計算します。

42
joel.neely

それらを生成するための順列アルゴリズムのその他の例。

出典: http://www.ddj.com/architect/201200326

  1. 既知の最速のアルゴリズムであるFikeのアルゴリズムを使用します。
  2. アルゴリズムを辞書順で使用します。
  3. 非レキソグラフィックを使用しますが、項目2よりも高速に実行されます。

1。


PROGRAM TestFikePerm;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] OF INTEGER;
    ii : INTEGER;
    permcount : INTEGER;

PROCEDURE WriteArray;
VAR i : INTEGER;
BEGIN
FOR i := 1 TO marksize
DO Write ;
WriteLn;
permcount := permcount + 1;
END;

PROCEDURE FikePerm ;
{Outputs permutations in nonlexicographic order.  This is Fike.s algorithm}
{ with tuning by J.S. Rohl.  The array marks[1..marksizn] is global.  The   }
{ procedure WriteArray is global and displays the results.  This must be}
{ evoked with FikePerm(2) in the calling procedure.}
VAR
    dn, dk, temp : INTEGER;
BEGIN
IF 
THEN BEGIN { swap the pair }
    WriteArray;
    temp :=marks[marksize];
    FOR dn :=  DOWNTO 1
    DO BEGIN
        marks[marksize] := marks[dn];
        marks [dn] := temp;
        WriteArray;
        marks[dn] := marks[marksize]
        END;
    marks[marksize] := temp;
    END {of bottom level sequence }
ELSE BEGIN
    FikePerm;
    temp := marks[k];
    FOR dk :=  DOWNTO 1
    DO BEGIN
        marks[k] := marks[dk];
        marks[dk][ := temp;
        FikePerm;
        marks[dk] := marks[k];
        END; { of loop on dk }
    marks[k] := temp;l
    END { of sequence for other levels }
END; { of FikePerm procedure }

BEGIN { Main }
FOR ii := 1 TO marksize
DO marks[ii] := ii;
permcount := 0;
WriteLn ;
WrieLn;
FikePerm ; { It always starts with 2 }
WriteLn ;
ReadLn;
END.

2。


PROGRAM TestLexPerms;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] OF INTEGER;
    ii : INTEGER;
    permcount : INTEGER;


PROCEDURE WriteArray; VAR i : INTEGER; BEGIN FOR i := 1 TO marksize DO Write ; permcount := permcount + 1; WriteLn; END;


PROCEDURE LexPerm ; { Outputs permutations in lexicographic order. The array marks is global } { and has n or fewer marks. The procedure WriteArray () is global and } { displays the results. } VAR work : INTEGER: mp, hlen, i : INTEGER; BEGIN IF THEN BEGIN { Swap the pair } work := marks[1]; marks[1] := marks[2]; marks[2] := work; WriteArray ; END ELSE BEGIN FOR mp := DOWNTO 1 DO BEGIN LexPerm<>; hlen := DIV 2; FOR i := 1 TO hlen DO BEGIN { Another swap } work := marks[i]; marks[i] := marks[n - i]; marks[n - i] := work END; work := marks[n]; { More swapping } marks[n[ := marks[mp]; marks[mp] := work; WriteArray; END; LexPerm<> END; END;


BEGIN { Main } FOR ii := 1 TO marksize DO marks[ii] := ii; permcount := 1; { The starting position is permutation } WriteLn < Starting position: >; WriteLn LexPerm ; WriteLn < PermCount is , permcount>; ReadLn; END.

3。


PROGRAM TestAllPerms;
CONST marksize = 5;
VAR
    marks : ARRAY [1..marksize] of INTEGER;
    ii : INTEGER;
    permcount : INTEGER;


PROCEDURE WriteArray; VAR i : INTEGER; BEGIN FOR i := 1 TO marksize DO Write ; WriteLn; permcount := permcount + 1; END;


PROCEDURE AllPerm (n : INTEGER); { Outputs permutations in nonlexicographic order. The array marks is } { global and has n or few marks. The procedure WriteArray is global and } { displays the results. } VAR work : INTEGER; mp, swaptemp : INTEGER; BEGIN IF THEN BEGIN { Swap the pair } work := marks[1]; marks[1] := marks[2]; marks[2] := work; WriteArray; END ELSE BEGIN FOR mp := DOWNTO 1 DO BEGIN ALLPerm<< n - 1>>; IF > THEN swaptemp := 1 ELSE swaptemp := mp; work := marks[n]; marks[n] := marks[swaptemp}; marks[swaptemp} := work; WriteArray; AllPerm< n-1 >; END; END;


BEGIN { Main } FOR ii := 1 TO marksize DO marks[ii] := ii permcount :=1; WriteLn < Starting position; >; WriteLn; Allperm < marksize>; WriteLn < Perm count is , permcount>; ReadLn; END.

Wikipedaで 順列の記事 を確認してください。また、 Factoradic 数という概念もあります。

とにかく、数学の問題はかなり難しいです。

C#iteratorを使用し、yieldを使用して置換アルゴリズムを停止できます。これの問題は、前後に移動したり、indexを使用したりできないことです。

3
Bogdan Maxim

clojure.contrib.lazy_seqsの順列関数はすでにこれを行うと主張しています。

2
Jason