順列を怠惰に生成する


87

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

あるいは、特定のセットが与えられると、そのセットの「次の」順列を返すアルゴリズムを探しています。これにより、独自の出力で関数を繰り返し呼び出すと、元のセットのすべての順列が循環します。いくつかの順序(順序は関係ありません)。

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

回答:


139

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

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

考えてみると「34125」であることがわかります。そして、あなたの考えはおそらくこれでした:「32541」では、

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

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

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

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

C ++コードはこれを正確に実行/usr/include/c++/4.0.0/bits/stl_algo.hします(システムのソースを確認するか、この記事を参照してください)。それをあなたの言語に翻訳するのは簡単なはずです:[C ++イテレータに慣れていない場合は、「BidirectionIterator」を「ポインタ」として読んでください。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」を与えます。


2
これは、要素の全順序があると仮定して機能します。
クリスコンウェイ

10
セットから始める場合は、要素の全順序を任意に定義できます。要素を個別の番号にマップします。:-)
ShreevatsaR

3
この回答は十分な賛成を得られませんが、私は一度しか賛成できません... :-)
Daniel C. Sobral

1
@Masse:正確ではありません...大まかに言って、1からもっと大きな数に変更できます。例を使用すると、32541から開始します。テールは541です。必要な手順を実行した後、次の順列は34125です。テールは5になります。5を使用して3412をインクリメントし、交換すると、次の順列は34152になります。 52、長さ2。すると、34215(テールの長さ1)、34251(テールの長さ2)、34512(長さ1)、34521(長さ3)、35124(長さ1)などになります。ほとんどの場合小さいため、アルゴリズムは複数の呼び出しに対して優れたパフォーマンスを発揮します。
ShreevatsaR 2010

1
@SamStoelinga:あなたは正しいです。O(n log n)はO(log n!)です。O(n!)と言うべきだった。
ShreevatsaR

42

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

  1. 要素の1つの順列を次の順列に変換する(ShreevatsaRが投稿したように)、または
  2. 0から上にn数えながら、th順列を直接計算し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)まで数え、対応する順列を直接計算します。


1
++あなたの答えは過小評価されています。受け入れられた答えを損なうことはありませんが、2番目のアプローチは、組み合わせにも一般化できるため、より強力です。完全な議論は、シーケンスからインデックスへの逆関数を示します。
先手で死ぬ

1
確かに。私は前のコメントに同意します—私の答えは尋ねられた特定の質問に対してわずかに少ない操作を行いますが、このアプローチはより一般的です。たとえば、与えられたものからKステップ離れた順列を見つけるために機能するからです。
ShreevatsaR 2010年

4

ウィキペディアの順列の記事を確認する必要があります。また、階乗進法の概念があります。

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

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


5
「とにかく、数学の問題はかなり難しいです。」いいえ、そうではありません:-)
ShreevatsaR

まあ、それは..階乗進法について知らなければ、許容できる時間内に適切なアルゴリズムを思い付く方法はありません。これは、方法を知らずに4次方程式を解こうとするようなものです。
ボグダンマキシム

1
申し訳ありませんが、元の問題について話していると思いました。とにかく「階乗数」が必要な理由はまだわかりません... nのそれぞれに番号を割り当てるのは非常に簡単です!与えられたセットの順列、および数から順列を構築します。[動的計画法/カウントのほんの一部..]
ShreevatsaR

1
慣用的なC#では、イテレーターはより正確には列挙子と呼ばれます
ドリューノアケス2011

@ShreevatsaR:すべての順列を生成する以外に、どのようにそれを行いますか?たとえば、n!番目の順列を生成する必要がある場合。
ジェイコブ

3

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

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

  1. 最も速く知られているアルゴリズムの1つであるFikeのアルゴリズムを使用します。
  2. アルゴを辞書式順序に使用します。
  3. 非語彙を使用しますが、項目2よりも高速に実行されます。

1.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.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.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.


2

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


おかげで、私はそれを知りませんでした。怠惰であると主張していますが、残念ながらパフォーマンスが非常に低く、スタックを簡単にオーバーフローします。
ブライアンカーパー

怠惰は、たとえばこの回答で説明されているように、確かにスタックオーバーフローを引き起こす可能性があります。
crockeea 2014
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.