Генерування перестановок ліниво


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.) ефективно, починаючи з кінця і повертаючись назад, доки попередній елемент не буде меншим за поточний елемент. Ви можете зробити (2.), просто помінявши "4" на "2", і ви отримаєте "34521". Після цього ви можете уникнути використання алгоритму сортування для (3.), оскільки хвіст було, і все ще (подумайте про це), відсортовано за зменшенням, тому його потрібно лише змінити.

Код 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
Це спрацює, припускаючи, що у вас є загальний порядок елементів.
Кріс Конвей

10
Якщо ви починаєте з набору, ви можете довільно визначити загальний порядок елементів; відобразити елементи в різні числа. :-)
ShreevatsaR

3
Ця відповідь просто не отримує достатньо голосів, але я можу лише один раз ... :-)
Даніель С. Собрал

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

42

Якщо припустити, що ми говоримо про лексикографічний порядок над переставленими значеннями, можна використати два загальних підходи:

  1. перетворити одну перестановку елементів на наступну перестановку (як розміщував ShreevatsaR), або
  2. безпосередньо обчислити nth-ту перестановку, відлічуючи при цьому 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

Для другого підходу (безпосереднього обчислення 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

Так, наприклад, 13-а перестановка ABCD знаходиться наступним чином:

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
++ Ваша відповідь недооцінена. Не відірвати від прийнятої відповіді, але другий підхід є більш потужним, оскільки його також можна узагальнити на комбінації. Повне обговорення покаже зворотну функцію від послідовності до індексу.
Помер у Сенте

1
Справді. Я погоджуюсь із попереднім коментарем - хоча моя відповідь робить трохи менше операцій для конкретного заданого питання, цей підхід є більш загальним, оскільки він працює, наприклад, для пошуку перестановки, яка знаходиться на відстані K від даної.
ShreevatsaR

4

Вам слід ознайомитися зі статтею « Пермутації» у вікіпеді. Також існує концепція факторадичних чисел.

У будь-якому випадку, математична задача досить складна.

У C#ви можете використовувати iterator, і зупинити алгоритм перестановки , використовуючи yield. Проблема цього полягає в тому, що ви не можете рухатися вперед і назад, або використовувати index.


5
"У будь-якому випадку, математична задача досить важка". Ні, це не так :-)
ShreevatsaR

Ну, це .. якщо ви не знаєте про числа Factoradic, жодним чином ви не зможете придумати належний алгоритм за прийнятний час. Це як намагатися розв’язати рівняння 4-го ступеня, не знаючи методу.
Богдан Максим

1
Ой, вибач, я думав, ти говориш про оригінальну проблему. Я все одно не розумію, навіщо вам потрібні "факторичні числа" ... досить просто призначити число кожному з n! перестановок даної множини та побудувати перестановку з числа. [Просто деяке динамічне програмування / підрахунок ..]
ShreevatsaR,

1
В ідіоматичній C # ітератор правильніше називати перелічувачем .
Drew Noakes

@ShreevatsaR: Як би ви зробили так, щоб не генерували всі перестановки? Наприклад, якщо вам потрібно було генерувати n! Ту перестановку.
Яків

3

Інші приклади алгоритмів перестановки для їх генерування.

Джерело: http://www.ddj.com/architect/201200326

  1. Використовує алгоритм Фіке, який є найшвидшим із відомих.
  2. Використовує Альго до лексографічного замовлення.
  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.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.