全排列生成算法:next_permutation

http://www.cnblogs.com/devymex/archive/2010/08/17/1801122.html

概念

全排列的生成算法有很多種,有遞歸遍例,也有循環移位法等等。C++/STL中定義的next_permutation和prev_permutation函數則是非常靈活且高效的一種方法,它被廣泛的應用於爲指定序列生成不同的排列。本文將詳細的介紹prev_permutation函數的內部算法。

按照STL文檔的描述,next_permutation函數將按字母表順序生成給定序列的下一個較大的序列,直到整個序列爲減序爲止。prev_permutation函數與之相反,是生成給定序列的上一個較小的序列。二者原理相同,僅遍例順序相反,這裏僅以next_permutation爲例介紹算法。

下文內容都基於一個假設,即序列中不存在相同元素。對序列大小的比較做出定義:兩個長度相同的序列,從兩者的第一個元素開始向後比較,直到出現一個不同元素(也可能就是第它們的第一個元素),該元素較大的序列爲大,反之序列爲小;若一直到最後一個元素都相同,那麼兩個序列相等。

設當前序列爲pn,下一個較大的序列爲pn+1,那麼不存在pm,使得pn < pm < pn+1

 

問題

給定任意非空序列,生成下一個較大或較小的序列。

 

數學推導

根據上述概念易知,對於一個任意序列,最小的序列是增序,最大的序列爲減序。那麼給定一個pn要如何才能生成pn+1呢?先來看下面的例子:

我們用<a1 a2 ... am>來表示m個數的一種序列。設序列pn=<3 6 4 2>,根據定義可算得下一個序列pn+1=<4 2 3 6>。觀察pn可以發現,其子序列<6 4 2>已經爲減序,那麼這個子序列不可能通過交換元素位置得出更大的序列了,因此必須移動最高位3(即a1)的位置,且要在子序列<6 4 2>中找一個數來取代3的位置。子序列<6 4 2>中6和4都比3大,但6大於4。如果用6去替換3得到的序列一定會大於4替換3得到的序列,因此只能選4。將4和3的位置對調後形成排列<4 6 3 2>。對調後得到的子序列<6 3 2>仍保持減序,即這3個數能夠生成的最大的一種序列。而4是第1次作爲首位的,需要右邊的子序列最小,因此4右邊的子序列應爲<2 3 6>,這樣就得到了正確的一個序列pn+1=<4 2 3 6>。

下面歸納分析該過程。假設一個有m個元素的序列pn,其下一個較大序列爲pn+1

1) 若pn最右端的2個元素構成一個增序子序列,那麼直接反轉這2個元素使該子序列成爲減序,即可得到pn+1

2) 若pn最右端一共有連續的s個元素構成一個減序子序列,令i = m - s,則有pn(i) < pn(i+1),其中pn(i)表示排列pn的第i個元素。例如pn=<1 2 5 4 3>,那麼pn的右端最多有3個元素構成一個減序子集<5 4 3>,i=5-3=2,則有pn(i)=2 < 5=pn(i+1)。因此若將pn(i)和其右邊的子集s {pn(i+1), pn(i+2), ..., pn(m)}中任意一個元素調換必能得到一個較大的序列(不一定是下一個)。要保證是下一個較大的序列,必須保持pn(i)左邊的元素不動,並在子集s {pn(i+1), pn(i+2), ..., pn(m)}中找出所有比pn(i)大的元素中最小的一個pn(j),即不存在pn(k) ∈ s且pn(i) < pn(k) < pn(j),然後將二者調換位置。現在只要使新子集{pn(i+1), pn(i+2), ..., pn(i), ...,pn(m)}成爲最小序列即得到pn+1。注意到新子集仍保持減序,那麼此時直接將其反轉即可得到pn+1 {pn(1), pn(2), ..., pn(j), pn(m), pn(m-1), ..., pn(i), ..., pn(i+2), pn(i+1)}。

 

複雜度

最好的情況爲pn的最右邊的2個元素構成一個最小的增序子集,交換次數爲1,複雜度爲O(1),最差的情況爲1個元素最小,而右面的所有元素構成減序子集,這樣需要先將第1個元素換到最右,然後反轉右面的所有元素。交換次數爲1+(n-1)/2,複雜度爲O(n)。因爲各種排列等可能出現,所以平均複雜度即爲O(n)。

 

擴展

1. 能否直接算出集合{1, 2, ..., m}的第n個排列?

設某個集合{a1, a2, ..., am}(a1<a2<...<am)構成的某種序列pn,基於以上分析易證得:若as<at,那麼將as作爲第1個元素的所有序列一定都小於at作爲第1個元素的任意序列。同理可證得:第1個元素確定後,剩下的元素中若as'<at',那麼將as'作爲第2個元素的所有序列一定都小於作爲第2個元素的任意序列。例如4個數的集合{2, 3, 4, 6}構成的序列中,以3作爲第1個元素的序列一定小於以4或6作爲第1個元素的序列;3作爲第1個元素的前題下,2作爲第2個元素的序列一定小於以4或6作爲第2個元素的序列。

推廣可知,在確定前i(i<n)個元素後,在剩下的m-i=s個元素的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm)中,以aqj作爲第i+1個元素的序列一定小於以aqj+1作爲第i+1個元素的序列。由此可知:在確定前i個元素後,一共可生成s!種連續大小的序列。

根據以上分析,對於給定的n(必有n<=m!)可以從第1位開始向右逐位地確定每一位元素。在第1位不變的前題下,後面m-1位一共可以生成(m-1)!中連續大小的序列。若n>(m-1)!,則第1位不會是a1,n中可以容納x個(m-1)!即代表第1位是ax。在確定第1位後,將第1位從原集合中刪除,得到新的集合{aq1, aq2, ..., aq3}(aq1<aq2<...<aqm),然後令n1=n-x(m-1)!,求這m-1個數中生成的第n1個序列的第1位。

舉例說明:如7個數的集合爲{1, 2, 3, 4, 5, 6, 7},要求出第n=1654個排列。

(1654 / 6!)取整得2,確定第1位爲3,剩下的6個數{1, 2, 4, 5, 6, 7},求第1654 % 6!=214個序列;

(214 / 5!)取整得1,確定第2位爲2,剩下5個數{1, 4, 5, 6, 7},求第214 % 5!=94個序列;

(94 / 4!)取整得3,確定第3位爲6,剩下4個數{1, 4, 5, 7},求第94 % 4!=22個序列;

(22 / 3!)取整得3,確定第4位爲7,剩下3個數{1, 4, 5},求第22 % 3!=4個序列;

(4 / 2!)得2,確定第5爲5,剩下2個數{1, 4};由於4 % 2!=0,故第6位和第7位爲增序<1 4>;

因此所有排列爲:3267514。

 

2. 給定一種排列,如何算出這是第幾個排列呢?

和前一個問題的推導過程相反。例如3267514:

後6位的全排列爲6!,3爲{1, 2, 3 ,4 , 5, 6, 7}中第2個元素(從0開始計數),故2*720=1440;

後5位的全排列爲5!,2爲{1, 2, 4, 5, 6, 7}中第1個元素,故1*5!=120;

後4位的全排列爲4!,6爲{1, 4, 5, 6, 7}中第3個元素,故3*4!=72;

後3位的全排列爲3!,7爲{1, 4, 5, 7}中第3個元素,故3*3!=18;

後2位的全排列爲2!,5爲{1, 4, 5}中第2個元素,故2*2!=4;

最後2位爲增序,因此計數0,求和得:1440+120+72+18+4=1654

 

C++/STL實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#include <algorithm>
#include <iostream>
#include <string>
using namespace std;
//主函數,算法詳見相關說明
int main(void) {
    //循環處理輸入的每一個字符串
    for (string str; cin >> str;) {
        if (str.empty()) {
            continue;
        }
        //如果字符串只有1個字符,則直接輸出結束
        if (str.length() <= 1) {
            cout << "No more Permutation" << endl;
        }
        //iPivot爲右邊最大減序子集左邊相鄰的一個元素
        string::iterator iPivot = str.end(), iNewHead;
        //查找右邊最大的減序子集
        for (--iPivot; iPivot != str.begin(); --iPivot) {
            if (*(iPivot - 1) <= *iPivot ) {
                break;
            }
        }
        //如果整個序列都爲減序,則重排結束。
        if (iPivot == str.begin()) {
            cout << "No more Permutation" << endl;
        }
        //iPivot指向子集左邊相鄰的一個元素
        iPivot--;
        //iNewHead爲僅比iPivot大的元素,在右側減序子集中尋找
        for (iNewHead = iPivot + 1; iNewHead != str.end(); ++iNewHead) {
            if (*iNewHead < *iPivot) {
                break;
            }
        }
        //交換iPivot和iNewHead的值,但不改變它們的指向
        iter_swap(iPivot, --iNewHead);
        //反轉右側減序子集,使之成爲最小的增序子集
        reverse(iPivot + 1, str.end());
        //本輪重排完成,輸出結果
        cout << str << endl;
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章