Tạo hoán vị một cách lười biếng


87

Tôi đang tìm một thuật toán để tạo ra các hoán vị của một tập hợp theo cách mà tôi có thể tạo một danh sách lười biếng về chúng trong Clojure. tức là tôi muốn lặp lại danh sách các hoán vị trong đó mỗi hoán vị không được tính cho đến khi tôi yêu cầu và tất cả các hoán vị không phải được lưu trữ trong bộ nhớ cùng một lúc.

Ngoài ra, tôi đang tìm một thuật toán trong đó đã cho một tập hợp nhất định, nó sẽ trả về hoán vị "tiếp theo" của tập hợp đó, theo cách liên tục gọi hàm trên đầu ra của chính nó sẽ xoay vòng qua tất cả các hoán vị của tập ban đầu, trong một số thứ tự (thứ tự là gì không quan trọng).

Có một thuật toán như vậy không? Hầu hết các thuật toán tạo hoán vị mà tôi đã thấy có xu hướng tạo tất cả chúng cùng một lúc (thường là đệ quy), điều này không mở rộng thành các tập rất lớn. Việc triển khai trong Clojure (hoặc một ngôn ngữ chức năng khác) sẽ hữu ích nhưng tôi có thể tìm ra nó từ mã giả.

Câu trả lời:


139

Có, có một "hoán vị tiếp theo" thuật toán, và nó khá đơn giản quá. Thư viện mẫu chuẩn C ++ (STL) thậm chí còn có một hàm được gọi next_permutation.

Thuật toán thực sự tìm ra hoán vị tiếp theo - hoán vị tiếp theo về mặt từ vựng. Ý tưởng là thế này: giả sử bạn được cung cấp một chuỗi, hãy nói "32541". Hoán vị tiếp theo là gì?

Nếu bạn nghĩ về nó, bạn sẽ thấy rằng nó là "34125". Và suy nghĩ của bạn có lẽ là điều gì đó: Trong "32541",

  • không có cách nào để giữ cố định "32" và tìm một hoán vị sau đó trong phần "541", bởi vì hoán vị đó đã là hoán vị cuối cùng cho 5,4, và 1 - nó được sắp xếp theo thứ tự giảm dần.
  • Vì vậy, bạn sẽ phải thay đổi "2" thành một cái gì đó lớn hơn - trên thực tế, thành số nhỏ nhất lớn hơn nó trong phần "541", cụ thể là 4.
  • Bây giờ, khi bạn đã quyết định rằng hoán vị sẽ bắt đầu là "34", các số còn lại sẽ theo thứ tự tăng dần, vì vậy câu trả lời là "34125".

Thuật toán là thực hiện chính xác dòng suy luận đó:

  1. Tìm "đuôi" dài nhất được sắp xếp theo thứ tự giảm dần. (Phần "541".)
  2. Thay đổi số ngay trước đuôi ("2") thành số nhỏ nhất lớn hơn nó ở đuôi (4).
  3. Sắp xếp đuôi theo thứ tự tăng dần.

Bạn có thể thực hiện (1.) một cách hiệu quả bằng cách bắt đầu từ cuối và quay ngược trở lại miễn là phần tử trước đó không nhỏ hơn phần tử hiện tại. Bạn có thể thực hiện (2) bằng cách hoán đổi số "4" với "2", vì vậy bạn sẽ có "34521". Khi bạn làm điều này, bạn có thể tránh sử dụng thuật toán sắp xếp cho (3), vì đuôi đã, và vẫn đang (hãy nghĩ về điều này), được sắp xếp theo thứ tự giảm dần, vì vậy nó chỉ cần được đảo ngược.

Mã C ++ thực hiện chính xác điều này (xem mã nguồn trong /usr/include/c++/4.0.0/bits/stl_algo.hhệ thống của bạn hoặc xem bài viết này ); nó phải đơn giản để dịch nó sang ngôn ngữ của bạn: [Đọc "BidirectionalIterator" là "con trỏ", nếu bạn không quen với các trình vòng lặp C ++. Mã trả về falsenếu không có hoán vị tiếp theo, tức là chúng ta đã có thứ tự giảm dần.]

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

Có vẻ như có thể mất O (n) thời gian cho mỗi hoán vị, nhưng nếu suy nghĩ kỹ hơn, bạn có thể chứng minh rằng tổng cộng phải mất O (n!) Thời gian cho tất cả các hoán vị, vì vậy chỉ O (1) - thời gian không đổi - mỗi hoán vị.

Điều tốt là thuật toán hoạt động ngay cả khi bạn có một chuỗi với các phần tử lặp lại: với, giả sử, "232254421", nó sẽ tìm đuôi là "54421", hoán đổi "2" và "4" (vì vậy "232454221" ), đảo ngược phần còn lại, cho ra "232412245", là hoán vị tiếp theo.


2
Điều này sẽ hoạt động, giả sử bạn có tổng thứ tự trên các phần tử.
Chris Conway

10
Nếu bạn bắt đầu với một tập hợp, bạn có thể tùy ý xác định thứ tự tổng trên các phần tử; ánh xạ các phần tử thành các số riêng biệt. :-)
ShreevatsaR

3
Câu trả lời này không nhận được đủ số phiếu ủng hộ, nhưng tôi chỉ có thể ủng hộ nó một lần ... :-)
Daniel C. Sobral

1
@Masse: Không chính xác ... đại khái là bạn có thể đi từ 1 đến số lớn hơn. Sử dụng ví dụ: Bắt đầu với 32541. Đuôi là 541. Sau khi thực hiện các bước cần thiết, hoán vị tiếp theo là 34125. Bây giờ đuôi chỉ là 5. Tăng 3412 bằng cách sử dụng 5 và đổi chỗ, hoán vị tiếp theo là 34152. Bây giờ đuôi là 52, chiều dài 2. Sau đó, nó trở thành 34215 (chiều dài đuôi 1), 34251 (chiều dài đuôi 2), 34512 (chiều dài 1), 34521 (chiều dài 3), 35124 (chiều dài 1), v.v. Bạn nói đúng rằng đuôi là hầu hết thời gian, đó là lý do tại sao thuật toán có hiệu suất tốt trên nhiều cuộc gọi.
ShreevatsaR

1
@SamStoelinga: Thực ra là bạn nói đúng. O (n log n) là O (log n!). Tôi nên nói O (n!).
ShreevatsaR

42

Giả sử rằng chúng ta đang nói về thứ tự từ vựng trên các giá trị được hoán vị, có hai cách tiếp cận chung mà bạn có thể sử dụng:

  1. biến đổi một hoán vị của các phần tử thành hoán vị tiếp theo (như ShreevatsaR đã đăng), hoặc
  2. tính trực tiếp nhoán vị thứ, đồng thời đếm ntừ 0 trở lên.

Đối với những người (như tôi ;-) không nói c ++ như người bản xứ, cách tiếp cận 1 có thể được thực hiện từ mã giả sau, giả sử lập chỉ mục dựa trên 0 của một mảng có chỉ mục 0 ở "bên trái" (thay thế một số cấu trúc khác , chẳng hạn như một danh sách, được "để lại như một bài tập" ;-):

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

Đây là một ví dụ bắt đầu với hoán vị hiện tại của 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

Đối với cách tiếp cận thứ hai (tính toán trực tiếp của nhoán vị thứ), hãy nhớ rằng có các N!hoán vị của Ncác phần tử. Do đó, nếu bạn là Ncác phần tử (N-1)!hoán vị , các hoán vị đầu tiên phải bắt đầu bằng phần tử nhỏ nhất, các (N-1)!hoán vị tiếp theo phải bắt đầu bằng phần tử nhỏ nhất thứ hai, v.v. Điều này dẫn đến cách tiếp cận đệ quy sau (một lần nữa trong mã giả, đánh số các hoán vị và vị trí từ 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

Vì vậy, ví dụ, hoán vị thứ 13 của ABCD được tìm thấy như sau:

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

Ngẫu nhiên, việc "loại bỏ" các phần tử có thể được biểu diễn bằng một mảng boolean song song cho biết phần tử nào vẫn còn sẵn dùng, do đó không cần thiết phải tạo một mảng mới trên mỗi lần gọi đệ quy.

Vì vậy, để lặp lại các hoán vị của ABCD, chỉ cần đếm từ 0 đến 23 (4! -1) và tính trực tiếp hoán vị tương ứng.


1
++ Câu trả lời của bạn không được đánh giá cao. Không phải để loại bỏ câu trả lời được chấp nhận, nhưng cách tiếp cận thứ hai mạnh hơn vì nó cũng có thể được khái quát hóa cho các kết hợp. Một cuộc thảo luận hoàn chỉnh sẽ cho thấy chức năng đảo ngược từ trình tự đến chỉ mục.
Chết trong Sente

1
Thật. Tôi đồng ý với nhận xét trước - mặc dù câu trả lời của tôi thực hiện ít thao tác hơn một chút cho câu hỏi cụ thể được hỏi, nhưng cách tiếp cận này tổng quát hơn, vì nó hoạt động để tìm hoán vị cách K bước so với một câu đã cho.
ShreevatsaR

4

Bạn nên xem bài viết về Hoán vị trên wikipeda. Ngoài ra, còn có khái niệm về số Factoradic .

Dù sao thì đề toán khá hóc búa.

Trong C#bạn có thể sử dụng một iteratorvà dừng thuật toán hoán vị bằng cách sử dụng yield. Vấn đề với điều này là bạn không thể quay lại và sử dụng một index.


5
"Dù sao thì đề toán khá khó." Không, không phải :-)
ShreevatsaR

Chà, đúng là .. nếu bạn không biết về số Factoradic thì không có cách nào bạn có thể đưa ra một thuật toán thích hợp trong một thời gian có thể chấp nhận được. Nó giống như cố gắng giải một phương trình bậc 4 mà không biết phương pháp.
Bogdan Maxim

1
Xin lỗi, tôi nghĩ bạn đang nói về vấn đề ban đầu. Tôi vẫn không hiểu tại sao bạn cần "Số thừa số" ... khá đơn giản để gán một số cho mỗi n! hoán vị của một tập hợp đã cho và để tạo một hoán vị từ một số. [Chỉ là một số lập trình động / đếm ..]
ShreevatsaR

1
Trong C # thành ngữ, một trình lặp được gọi đúng hơn là một điều tra viên .
Drew Noakes

@ShreevatsaR: Bạn sẽ làm thế nào để tạo ra tất cả các hoán vị? Ví dụ: nếu bạn cần tạo hoán vị thứ n.
Jacob

3

Thêm ví dụ về thuật toán hoán vị để tạo ra chúng.

Nguồn: http://www.ddj.com/architect/201200326

  1. Sử dụng Thuật toán Fike, một trong những thuật toán nhanh nhất được biết đến.
  2. Sử dụng Algo để đặt hàng Lexographic.
  3. Sử dụng phần mềm không in ấn, nhưng chạy nhanh hơn mục 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.


2

hàm hoán vị trong clojure.contrib.lazy_seqs đã tuyên bố chỉ làm điều này.


Cảm ơn, tôi đã không biết về nó. Nó tuyên bố là lười biếng, nhưng đáng buồn là nó hoạt động rất kém và dễ dàng làm tràn ngăn xếp.
Brian Carper 10/08/08

Ví dụ, sự lười biếng chắc chắn có thể gây ra tràn ngăn xếp như đã giải thích trong câu trả lời này .
crockeea
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.