느리게 순열 생성


88

Clojure에서 게으른 목록을 만들 수있는 방식으로 집합의 순열을 생성하는 알고리즘을 찾고 있습니다. 즉, 요청할 때까지 각 순열이 계산되지 않는 순열 목록을 반복하고 모든 순열을 한 번에 메모리에 저장할 필요가 없습니다.

또는 특정 집합이 주어지면 해당 집합의 "다음"순열을 반환하는 알고리즘을 찾고 있습니다. 이러한 방식으로 자체 출력에서 ​​함수를 반복적으로 호출하면 원래 집합의 모든 순열이 순환됩니다. 어떤 순서 (순서는 중요하지 않습니다).

그러한 알고리즘이 있습니까? 내가 본 대부분의 순열 생성 알고리즘은 한 번에 (일반적으로 재귀 적으로) 생성하는 경향이 있으며, 이는 매우 큰 집합으로 확장되지 않습니다. Clojure (또는 다른 기능적 언어)로 구현하면 도움이되지만 의사 코드에서 알아낼 수 있습니다.

답변:


140

네, 이다 는 "다음 순열"알고리즘, 그것도 아주 간단합니다. 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 ++ 반복자에 익숙하지 않은 경우 "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"를 제공합니다.


2
요소에 대한 총 주문이 있다고 가정하면 작동합니다.
Chris Conway

10
세트로 시작하면 요소의 총 순서를 임의로 정의 할 수 있습니다. 요소를 고유 한 숫자에 매핑합니다. :-)
ShreevatsaR

3
이 답변은 충분히 upvotes을 얻을하지 않습니다,하지만 난 한 번만 ... :-)를 upvote에 수
다니엘 C. 소브랄

1
@Masse : 정확히는 아니지만 ... 대략 1에서 더 큰 숫자로 갈 수 있습니다. 예를 사용하면 32541로 시작합니다. 꼬리는 541입니다. 필요한 단계를 수행 한 후 다음 순열은 34125입니다. 이제 꼬리는 5입니다. 5와 스와핑을 사용하여 3412를 증가 시키면 다음 순열은 34152입니다. 이제 꼬리는 다음과 같습니다. 52, 길이 2입니다. 그러면 34215 (꼬리 길이 1), 34251 (꼬리 길이 2), 34512 (길이 1), 34521 (길이 3), 35124 (길이 1) 등이됩니다. 꼬리가 맞습니다. 대부분의 시간이 짧기 때문에 알고리즘이 여러 호출에 대해 우수한 성능을 발휘합니다.
ShreevatsaR

1
@SamStoelinga : 실제로 당신이 맞습니다. O (n log n)는 O (log n!)입니다. 나는 O (n!)라고 말 했어야했다.
ShreevatsaR

43

순열되는 값에 대한 사전 식 순서에 대해 이야기하고 있다고 가정하면 다음과 같은 두 가지 일반적인 접근 방식을 사용할 수 있습니다.

  1. 요소의 하나의 순열을 다음 순열로 변환합니다 (ShreevatsaR 게시 됨).
  2. 0에서 위로 n세면서 th 순열을 직접 계산합니다 n.

C ++를 네이티브로 사용하지 않는 사람들 (나와 같은 ;-)의 경우, "왼쪽"에 인덱스 0이있는 배열의 제로 기반 인덱싱을 가정하여 다음 의사 코드에서 접근 방식 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

두 번째 접근법 ( nth 순열 의 직접 계산)의 경우 요소 N!순열 이 있음을 기억하십시오 N. 따라서 N요소를 (N-1)!순열 하는 경우 첫 번째 순열은 가장 작은 요소로 (N-1)!시작해야 하고 다음 순열은 두 번째로 작은 순열로 시작해야합니다. 이로 인해 다음과 같은 재귀 적 접근 방식이 발생합니다 (다시 의사 코드에서 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
++ 귀하의 답변은 저평가되어 있습니다. 받아 들여지는 대답을 빼앗기지 말고 두 번째 접근 방식은 조합으로 일반화 될 수 있기 때문에 더 강력합니다. 완전한 논의는 시퀀스에서 인덱스로의 역기능을 보여줄 것입니다.
Sente

2
과연. 나는 이전 의견에 동의합니다. 내 대답이 요청한 특정 질문에 대해 약간의 작업을 수행하더라도이 접근 방식은 더 일반적입니다. 예를 들어 주어진 질문에서 K 단계 떨어져있는 순열을 찾는 데 효과적이기 때문입니다.
ShreevatsaR

4

wikipeda 의 순열 기사 를 확인해야합니다 . 또한 Factoradic number 의 개념이 있습니다.

어쨌든 수학적 문제는 꽤 어렵습니다.

에서 C#를 사용 iterator하고을 사용하여 순열 알고리즘을 중지 할 수 있습니다 yield. 이것의 문제는 당신이 앞뒤로 갈 수 없거나 index.


5
"어쨌든 수학적 문제는 꽤 어렵습니다." 아니요 :-)
ShreevatsaR

음, 그건 .. 만약 당신이 Factoradic 숫자에 대해 모른다면 당신이 적당한 시간에 적절한 알고리즘을 내놓을 수있는 방법은 없습니다. 방법을 모르고 4 차 방정식을 풀려고하는 것과 같습니다.
Bogdan Maxim

1
죄송합니다. 원래 문제에 대해 얘기하는 줄 알았어요. 어차피 왜 "요인 수"가 필요한지 모르겠네요. 각 n에 숫자를 할당하는 것은 매우 간단합니다! 주어진 세트의 순열, 그리고 숫자에서 순열을 구성합니다. [그냥 동적 프로그래밍 / 계산 ..]
ShreevatsaR

1
관용적 C #에서는 반복기를 열거 자라고하는 것이 더 정확합니다 .
Drew Noakes 2011

@ShreevatsaR : 모든 순열을 생성하는 데 어떻게 그렇게 부족합니까? 예를 들어 n 번째 순열을 생성해야하는 경우.
Jacob

3

이를 생성하기위한 순열 알고리즘의 더 많은 예.

출처 : http://www.ddj.com/architect/201200326

  1. 가장 빨리 알려진 Fike의 알고리즘을 사용합니다.
  2. 사전 순서에 Algo를 사용합니다.
  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.

삼.


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의 순열 함수는 이미 이것을 수행한다고 주장합니다.


고마워, 몰랐어. 게으르다 고 주장하지만 슬프게도 성능이 매우 떨어지고 스택이 쉽게 오버플로됩니다.
Brian Carper

게으름은, 예를 들어 설명 스택 오버 플로우 확실히 원인이 될 수 있습니다 대답.
crockeea 2014-08-25
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.