สร้างการเรียงสับเปลี่ยนอย่างเฉื่อยชา


88

ฉันกำลังมองหาอัลกอริทึมเพื่อสร้างการเรียงสับเปลี่ยนของชุดในลักษณะที่ฉันสามารถสร้างรายการขี้เกียจใน Clojure ได้ เช่นฉันต้องการวนซ้ำรายการของการเรียงสับเปลี่ยนโดยที่การเรียงสับเปลี่ยนแต่ละรายการจะไม่ถูกคำนวณจนกว่าฉันจะร้องขอและการเรียงสับเปลี่ยนทั้งหมดไม่จำเป็นต้องเก็บไว้ในหน่วยความจำในครั้งเดียว

หรือฉันกำลังมองหาอัลกอริทึมที่กำหนดชุดหนึ่งมันจะส่งคืนการเรียงสับเปลี่ยน "ถัดไป" ของชุดนั้นในลักษณะที่เรียกฟังก์ชันซ้ำ ๆ บนเอาต์พุตของตัวเองจะวนรอบการเรียงสับเปลี่ยนทั้งหมดของชุดเดิมใน คำสั่งบางอย่าง (คำสั่งซื้อใดไม่สำคัญ)

มีอัลกอริทึมดังกล่าวหรือไม่? อัลกอริธึมการสร้างการเปลี่ยนแปลงส่วนใหญ่ที่ฉันเคยเห็นมักจะสร้างทั้งหมดพร้อมกัน (โดยปกติจะเรียกซ้ำ) ซึ่งไม่ได้ปรับขนาดเป็นชุดใหญ่มาก การใช้งานใน Clojure (หรือภาษาที่ใช้งานได้อื่น) จะเป็นประโยชน์ แต่ฉันสามารถหาได้จาก pseudocode

คำตอบ:


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. ) ได้อย่างมีประสิทธิภาพโดยเริ่มจากจุดสิ้นสุดและถอยหลังตราบเท่าที่องค์ประกอบก่อนหน้าไม่เล็กกว่าองค์ประกอบปัจจุบัน คุณสามารถทำได้ (2. ) โดยเพียงแค่สลับ "4" กับ "2" คุณก็จะมี "34521" เมื่อคุณทำเช่นนี้คุณสามารถหลีกเลี่ยงการใช้อัลกอริทึมการเรียงลำดับสำหรับ (3. ) ได้เนื่องจาก tail เป็นและนิ่ง (ลองคิดดูสิ) เรียงตามลำดับที่ลดลงจึงจำเป็นต้องย้อนกลับเท่านั้น

รหัส C ++ ทำสิ่งนี้ได้อย่างแม่นยำ (ดูที่ซอร์สใน/usr/include/c++/4.0.0/bits/stl_algo.hระบบของคุณหรือดูบทความนี้ ) ควรแปลเป็นภาษาของคุณได้ง่าย: [อ่าน "BidirectionalIterator" เป็น "ตัวชี้" หากคุณไม่คุ้นเคยกับตัววนซ้ำ C ++ โค้ดจะส่งคืน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
คำตอบนี้ไม่ได้รับการโหวตมากพอ แต่ฉันสามารถโหวตได้เพียงครั้งเดียว ... :-)
Daniel C. Sobral

1
@ Masse: ไม่ตรง ... คร่าวๆคุณสามารถเปลี่ยนจาก 1 ไปเป็นจำนวนที่มากกว่าได้ ใช้ตัวอย่าง: เริ่มต้นด้วย 32541 หางคือ 541 หลังจากทำตามขั้นตอนที่จำเป็นแล้วการเปลี่ยนแปลงต่อไปคือ 34125 ตอนนี้หางเป็นเพียง 5 การเพิ่ม 3412 โดยใช้ 5 และการแลกเปลี่ยนการเรียงสับเปลี่ยนต่อไปคือ 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. คำนวณการnเรียงสับเปลี่ยน th โดยตรงในขณะที่นับnจาก 0 ขึ้นไป

สำหรับผู้ที่ (เช่นฉัน ;-) ที่ไม่พูด 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

สำหรับแนวทางที่สอง (การคำนวณโดยตรงของการnเรียงสับเปลี่ยน th) โปรดจำไว้ว่ามี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
++ คำตอบของคุณไม่ได้รับการยอมรับ ไม่ใช่เพื่อหลีกหนีจากคำตอบที่ยอมรับ แต่แนวทางที่สองมีประสิทธิภาพมากกว่าเพราะสามารถสรุปรวมกับชุดค่าผสมได้เช่นกัน การอภิปรายที่สมบูรณ์จะแสดงฟังก์ชันย้อนกลับจากลำดับเป็นดัชนี
Die in Sente

2
แน่นอน. ฉันเห็นด้วยกับความคิดเห็นก่อนหน้านี้ - แม้ว่าคำตอบของฉันจะดำเนินการน้อยลงเล็กน้อยสำหรับคำถามเฉพาะที่ถาม แต่วิธีการนี้มีความกว้างมากขึ้นเนื่องจากใช้ได้กับการค้นหาการเรียงสับเปลี่ยนที่อยู่ห่างจากขั้นตอน K
ShreevatsaR

4

คุณควรตรวจสอบบทความ Permutationsใน wikipeda นอกจากนี้ยังมีแนวคิดของตัวเลขFactoradic

อย่างไรก็ตามปัญหาทางคณิตศาสตร์ค่อนข้างยาก

ในC#คุณสามารถใช้iteratorและหยุดอัลกอริทึมการเปลี่ยนแปลงโดยใช้yield. ปัญหานี้คือคุณไม่สามารถย้อนกลับไปมาหรือใช้indexไฟล์.


5
"อย่างไรก็ตามปัญหาทางคณิตศาสตร์ค่อนข้างยาก" ไม่มันไม่ :-)
ShreevatsaR

ก็คือ .. ถ้าคุณไม่รู้เกี่ยวกับตัวเลข Factoradic ก็ไม่มีทางที่คุณจะสามารถสร้างอัลกอริทึมที่เหมาะสมได้ในเวลาที่ยอมรับได้ มันเหมือนกับการพยายามแก้สมการองศาที่ 4 โดยไม่รู้วิธี
Bogdan Maxim

1
ขออภัยฉันคิดว่าคุณกำลังพูดถึงปัญหาเดิม ฉันยังไม่เห็นว่าทำไมคุณถึงต้องใช้ "ตัวเลขแฟกเตอร์" อยู่ดี ... มันค่อนข้างง่ายที่จะกำหนดตัวเลขให้กับแต่ละ n! การเรียงสับเปลี่ยนของชุดที่กำหนดและสร้างการเรียงสับเปลี่ยนจากตัวเลข [การเขียนโปรแกรมแบบไดนามิกบางส่วน / การนับ .. ]
ShreevatsaR

1
ในสำนวน C # เป็น iterator มากขึ้นอย่างถูกต้องเรียกว่าการแจงนับ
Drew Noakes

@ShreevatsaR: คุณจะทำอย่างไรในการสร้างการเรียงสับเปลี่ยนทั้งหมด? เช่นถ้าคุณต้องการสร้างการเปลี่ยนแปลง n! th
Jacob

3

ตัวอย่างเพิ่มเติมของอัลกอริทึมการเปลี่ยนแปลงเพื่อสร้าง

ที่มา: http://www.ddj.com/architect/201200326

  1. ใช้อัลกอริทึมของ Fike ซึ่งเป็นที่รู้จักกันเร็วที่สุด
  2. ใช้ Algo ตามคำสั่ง Lexographic
  3. ใช้ nonlexographic แต่ทำงานได้เร็วกว่าข้อ 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

ฟังก์ชันการเรียงสับเปลี่ยนใน clojure.contrib.lazy_seqs อ้างว่าทำสิ่งนี้อยู่แล้ว


ขอบคุณฉันไม่ทราบ มันอ้างว่าขี้เกียจ แต่น่าเศร้าที่มันทำงานได้แย่มากและล้นสแต็กได้อย่างง่ายดาย
Brian Carper

ความเกียจคร้านได้อย่างแน่นอนสาเหตุสแต็คล้นตามที่อธิบายไว้ตัวอย่างเช่นในนี้คำตอบ
crockeea
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.