Tembel permütasyon oluşturma


88

Clojure'da tembel bir liste yapabileceğim bir kümenin permütasyonlarını oluşturmak için bir algoritma arıyorum. Yani, her permütasyonun ben talep edene kadar hesaplanmadığı ve tüm permütasyonların aynı anda bellekte saklanmasının gerekmediği bir permütasyon listesi üzerinde yinelemek istiyorum.

Alternatif olarak, belirli bir küme verildiğinde bir algoritma arıyorum, bu kümenin "sonraki" permütasyonunu döndürecektir, öyle ki, işlevi kendi çıktısında tekrar tekrar çağırmak, orijinal kümenin tüm permütasyonları arasında döngü yapacaktır. bir düzen (sıranın ne olduğu önemli değil).

Böyle bir algoritma var mı? Gördüğüm permütasyon üreten algoritmaların çoğu, hepsini aynı anda (genellikle yinelemeli olarak) üretme eğilimindedir, bu da çok büyük kümelere ölçeklenmez. Clojure'da (veya başka bir işlevsel dilde) bir uygulama yardımcı olabilir, ancak bunu sözde koddan anlayabilirim.

Yanıtlar:


140

Evet, orada olan bir "sonraki permütasyon" algoritması ve çok çok basit. C ++ standart şablon kitaplığının (STL) bile adı verilen bir işlevi vardırnext_permutation .

Algoritma aslında bir sonraki permütasyonu bulur - sözlükbilimsel olarak sonraki permütasyonu. Fikir şudur: bir sıra verildiğini varsayalım, "32541" deyin. Bir sonraki permütasyon nedir?

Düşünürseniz, "34125" olduğunu görürsünüz. Ve düşünceleriniz muhtemelen şuydu: "32541" de,

  • "32" yi sabit tutmanın ve "541" bölümünde sonraki bir permütasyonu bulmanın bir yolu yoktur, çünkü bu permütasyon zaten 5,4 ve 1 için sonuncudur - azalan sırada sıralanır.
  • Bu yüzden "2" yi daha büyük bir şeye - aslında "541" bölümünde ondan daha büyük olan en küçük sayıya, yani 4'e değiştirmeniz gerekecek.
  • Şimdi, permütasyonun "34" olarak başlayacağına karar verdiğinizde, sayıların geri kalanı artan sırada olmalıdır, yani cevap "34125" olacaktır.

Algoritma, tam olarak bu akıl yürütme çizgisini uygulamaktır:

  1. Azalan sırada sıralanan en uzun "kuyruğu" bulun. ("541" bölümü.)
  2. Kuyruğun hemen önündeki sayıyı ("2") kuyrukta olduğundan daha büyük olan en küçük sayıya (4) değiştirin.
  3. Kuyruğu artan sırayla sıralayın.

Sondan başlayarak ve önceki öğe mevcut öğeden daha küçük olmadığı sürece geriye doğru giderek (1.) verimli bir şekilde yapabilirsiniz. (2.) 'i' 2 'ile değiştirerek yapabilirsiniz, böylece "34521" e sahip olursunuz. Bunu yaptıktan sonra, (3.) için bir sıralama algoritması kullanmaktan kaçınabilirsiniz çünkü kuyruk azalan düzende sıralandı ve hala (bunu düşünün), bu nedenle yalnızca tersine çevrilmesi gerekiyor.

C ++ kodu tam olarak bunu yapar ( /usr/include/c++/4.0.0/bits/stl_algo.hsisteminizdeki kaynağa bakın veya bu makaleye bakın ); bunu kendi dilinize çevirmek basit olmalıdır: [C ++ yineleyicilerine aşina değilseniz, "BidirectionalIterator" ı "işaretçi" olarak okuyun. falseSonraki permütasyon yoksa kod geri döner , yani zaten azalan sıradayız.]

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

Permütasyon başına O (n) süre alabilir gibi görünebilir, ancak daha dikkatli düşünürseniz, toplamda tüm permütasyonlar için O (n!) Zaman aldığını kanıtlayabilirsiniz, bu nedenle yalnızca O (1) - sabit zaman - permütasyon başına.

İşin iyi yanı, algoritmanın tekrarlanan öğeler içeren bir diziye sahip olduğunuzda bile çalışmasıdır: "232254421" ile, kuyruğu "54421" olarak bulur, "2" ve "4 "'ü değiştirir (yani" 232454221 " ), bir sonraki permütasyon olan "232412245" i vererek geri kalanı tersine çevirin.


2
Bu, öğeler üzerinde toplam bir siparişiniz olduğunu varsayarak çalışacaktır.
Chris Conway

10
Bir küme ile başlarsanız, elemanlar üzerinde rastgele bir toplam düzen tanımlayabilirsiniz; öğeleri farklı sayılarla eşleyin. :-)
ShreevatsaR

3
Bu yanıt yeterince olumlu oy almıyor, ancak yalnızca bir kez yükseltebilirim ... :-)
Daniel C. Sobral

1
@Masse: Tam olarak değil ... kabaca 1'den daha büyük bir sayıya gidebilirsiniz. Örneği kullanarak: 32541 ile başlayın. Kuyruk 541'dir. Gerekli adımları uyguladıktan sonra, sonraki permütasyon 34125'tir. Şimdi kuyruk sadece 5'tir. 52, uzunluk 2. Sonra 34215 (kuyruk uzunluğu 1), 34251 (kuyruk uzunluğu 2), 34512 (uzunluk 1), 34521 (uzunluk 3), 35124 (uzunluk 1) vb. Olur. Kuyruğun çoğu zaman küçüktür, bu nedenle algoritma birden fazla aramada iyi performans gösterir.
ShreevatsaR

1
@SamStoelinga: Aslında haklısın. O (n log n) O (log n!) O (n!) Demeliydim.
ShreevatsaR

43

İzin verilen değerler üzerindeki sözlük düzeninden bahsettiğimizi varsayarsak, kullanabileceğiniz iki genel yaklaşım vardır:

  1. elementlerin bir permütasyonunu bir sonraki permütasyona dönüştürmek (ShreevatsaR'ın yayınladığı gibi) veya
  2. 0'dan yukarıya doğru nsayarken doğrudan permütasyonu hesaplayın n.

Yerel olarak c ++ 'ı konuşmayanlar için (benim gibi ;-) yaklaşım 1, aşağıdaki sözde koddan uygulanabilir, "solda" indeks sıfır olan bir dizinin sıfır tabanlı indekslenmesi varsayılır (başka bir yapının yerine , bir liste gibi "alıştırma olarak bırakılır" ;-):

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'nin mevcut permütasyonuyla başlayan bir örnek:

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

İkinci yaklaşım için (. nPermütasyonun doğrudan hesaplanması ), elementlerin N!permütasyonları olduğunu unutmayın N. Bu nedenle, Nelementleri değiştiriyorsanız , ilk(N-1)! permütasyon permütasyonlar en küçük elementle (N-1)!başlamalı , sonraki permütasyonlar ikinci en küçükle başlamalı, vb. Bu, aşağıdaki özyinelemeli yaklaşıma yol açar (yine sözde kodda, permütasyonların ve konumların 0'dan numaralandırılması):

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

Dolayısıyla, örneğin, ABCD'nin 13. permütasyonu aşağıdaki gibi bulunur:

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

Bu arada, öğelerin "kaldırılması", hangi öğelerin hala kullanılabilir olduğunu gösteren paralel bir mantık dizisi ile temsil edilebilir, bu nedenle her özyinelemeli çağrıda yeni bir dizi oluşturmak gerekli değildir.

Dolayısıyla, ABCD'nin permütasyonlarını yinelemek için, 0'dan 23'e (4! -1) kadar sayın ve doğrudan karşılık gelen permütasyonu hesaplayın.


1
++ Cevabınız yeterince takdir edilmiyor. Kabul edilen cevaptan uzaklaşmamak, ancak ikinci yaklaşım daha güçlüdür çünkü kombinasyonlara da genellenebilir. Tam bir tartışma, diziden dizine ters işlevi gösterecektir.
Sente'de Öl

2
Aslında. Önceki yoruma katılıyorum - cevabım sorulan belirli soru için biraz daha az işlem yapsa da, bu yaklaşım daha geneldir, çünkü örneğin K adımı uzaktaki permütasyonu bulmak için işe yarar.
ShreevatsaR

4

Wikipeda ile ilgili Permütasyonlar makalesine bakmalısınız. Ayrıca, Factoradic kavramı var sayılar .

Her neyse, matematiksel problem oldukça zor.

İçinde C#bir kullanabilir iteratorve permütasyon algoritmasını kullanarak durdurabilirsiniz.yield . Bununla ilgili sorun, ileri geri gidememeniz veya bir index.


5
"Her neyse, matematik problemi oldukça zor." Hayır :-)
ShreevatsaR

Şey, öyle .. Eğer Faktöradik sayıları bilmiyorsanız, kabul edilebilir bir zamanda uygun bir algoritma bulmanızın bir yolu yoktur. Yöntemi bilmeden 4. derece denklemi çözmeye çalışmak gibi.
Bogdan Maxim

1
Üzgünüm, asıl sorundan bahsettiğini sanıyordum. Yine de neden "Faktöradik sayılara" ihtiyacınız olduğunu anlamıyorum ... n'nin her birine bir sayı atamak oldukça basit! belirli bir kümenin permütasyonları ve bir sayıdan bir permütasyon oluşturmak. [Sadece biraz dinamik programlama / sayma ..]
ShreevatsaR

1
Deyimsel C # 'ta yineleyici daha doğru bir şekilde numaralandırıcı olarak adlandırılır .
Drew Noakes

@ShreevatsaR: Tüm permütasyonları oluşturmadan bu kadar kısa sürede nasıl yaparsınız? Örneğin, n! İnci permütasyonu oluşturmanız gerekirse.
Jacob

3

Bunları oluşturmak için daha fazla permütasyon algoritması örneği.

Kaynak: http://www.ddj.com/architect/201200326

  1. Bilinen en hızlı algoritma olan Fike Algoritmasını kullanır.
  2. Algo'yu Lexografik sıraya göre kullanır.
  3. Leksografik olmayanı kullanır, ancak öğe 2'den daha hızlı çalışır.

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'deki permütasyonlar işlevi zaten bunu yaptığını iddia ediyor.


Teşekkürler, farkında değildim. Tembel olduğunu iddia ediyor, ancak ne yazık ki çok kötü çalışıyor ve yığını kolayca taşıyor.
Brian Carper

Tembellik, örneğin bu cevapta açıklandığı gibi kesinlikle yığın taşmasına neden olabilir .
crockeea
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.