懒惰地生成排列


87

我正在寻找一种算法来生成集合的排列,以便可以在Clojure中列出它们的惰性列表。即,我想遍历一系列排列,在我请求之前不会计算每个排列,并且不必将所有排列立即存储在内存中。

或者,我正在寻找一种算法,给定特定集合,该算法将返回该集合的“下一个”排列,以这种方式,在其自己的输出上重复调用该函数将循环遍历原始集合的所有排列,一些订单(顺序无关紧要)。

有这样的算法吗?我见过的大多数置换生成算法都倾向于一次全部生成它们(通常是递归生成),而这些算法不能扩展到很大的集合。用Clojure(或另一种功能语言)实现可能会有所帮助,但我可以从伪代码中弄清楚。

Answers:


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.)。您只需将“ 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
假设您对这些元素有总顺序,这将起作用。
克里斯·康威,

10
如果从集合开始,则可以任意定义元素的总顺序。将元素映射到不同的数字。:-)
ShreevatsaR

3
这个答案只是没有足够的投票,但我只能投票一次... :-)
Daniel C. Sobral

1
@Masse:不完全是……您可以从1到更大的数字。以示例为例:从32541开始。尾部为541。完成必要的步骤后,下一个排列为34125。现在尾部为5。使用5递增3412并交换,下一个排列为34152。现在尾部为长度2的52。然后变成34215(尾巴长度1),34251(尾巴长度2),34512(长度1),34521(长度3),35124(长度1),等等。大部分时间都很少,这就是为什么该算法在多次调用中都具有良好的性能。
ShreevatsaR

1
@SamStoelinga:实际上,您是对的。O(n log n)是O(log n!)。我应该说O(n!)。
ShreevatsaR 2013年

42

假设我们正在谈论排列的值的字典顺序,则可以使用两种通用方法:

  1. 将元素的一个排列转换为下一个排列(如ShreevatsaR发布),或者
  2. 直接计算 nn从0向上计数时,th排列。

对于那些不像本地人那样讲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

因此,例如,发现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
++您的答案被低估了。不能脱离公认的答案,但是第二种方法功能更强大,因为它也可以泛化为组合。完整的讨论将显示从序列到索引的反向功能。
死于森特

1
确实。我同意前面的评论-尽管我的回答对所提出的特定问题所做的操作略少,但这种方法更为通用,因为它适用于例如查找与给定变量相距K步的排列。
ShreevatsaR

4

您应该查看Wikipeda上的置换文章。另外,还有阶乘数的概念。

无论如何,数学问题是很难的。

在中,C#您可以使用和iterator,并使用停止排列算法yield。问题是您无法来回移动或使用index


5
“无论如何,数学问题很难。” 不,不是:-)
ShreevatsaR

好吧,这是..如果您不知道Factoradic数,就不可能在可接受的时间内提出合适的算法。这就像在不知道方法的情况下尝试求解四阶方程。
Bogdan Maxim

1
哦,对不起,我以为您在谈论原始问题。无论如何,我仍然不明白为什么需要“ Factoradic数字” ...为n中的每个数字分配数字非常简单!给定集合的排列,并根据数字构造排列。[只是一些动态编程/计数..]
ShreevatsaR

1
在惯用的C#中,将迭代器更正确地称为枚举器
德鲁·诺阿克斯

@ShreevatsaR:除了生成所有排列之外,您将如何做?例如,如果您需要生成第n个排列。
2013年

3

生成置换算法的更多示例。

来源:http//www.ddj.com/architect/201200326

  1. 使用Fike的算法,这是已知最快的算法之一。
  2. 将算法用于Lexographic顺序。
  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 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.