1. 程式人生 > >暴力求解法中的列舉排列,生成全排列

暴力求解法中的列舉排列,生成全排列

  **對於一個長度為n陣列長度的陣列={0,1,2,3,..., n-1}。要想列舉它的所有的長度為n的全排列出來。**
  有兩種選擇:一個是直接列舉, 另外一個是使用遞迴來構造。
  先說最容易理解的直接列舉。 例如當 n=5 的時候,我們生成的排列從小到大有 1234,1243,1324,..... ,4321。很容易地,我們可以用4個for迴圈搞定。
  程式碼如下:
#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace
std; int main() { int n = 5; for(int a = 0; a < n; a++) { for(int b = 0; b < n; b++) { if(b == a) continue; for(int c = 0; c < n; c++) { if(c == a || c == b) continue; for(int d = 0; d < n; d++) { if
(d == a || d == b || d == c) continue; cout <<a<<b<<c<<d<<endl; } } } } return 0; }
    在這個演算法裡面我們用*continue*來直接跳過輸出重複的排列,避免運算不必要的解,從而降低時間複雜度。但是這個演算法有遇到 n 更大的時候就要寫 n-1個for迴圈,看過《啊哈演算法》的都知道 裡面有一個9個for迴圈的求解。。。。。。這個就不用多說了。
  繼續討論n更加大的時候的問題, 前面說了,當n等於10的時候我們不至於真的去寫9個for迴圈吧, 這時候我們可以用遞迴構造的方法來列舉。
  遞迴函式: print_permutation(int n, int *A, int cur)
  遞迴使用需要有判定界限,定義一個數組A,往A[]裡面放我們需要排列的元素P[], 我們的判定界限則是: 當 n=cur 時 return 。
  程式碼如下:
#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
void print_permutation(int n, int* A, int cur)
{
    if(n == cur)//排列到n個長度後輸出
    {
        for(int i = 0; i < n; i++)
            cout << A[i];
        cout << endl;
        return;
    }
    else
    {
        for(int i = 0; i < n; i++)
        {
            int ok = 1;
            for(int j = 0; j < cur; j++)
            //判斷i是不是已經使用過了
                if(A[j] == i)
                    ok = 0;
            if(ok)
            {
                A[cur] = i;
                print_permutation(n, A, cur + 1);
            }
        }
    }
}
int main()
{
    int P[100], A[100] = {0};
    int n;
    cin >> n;
    cout << "0 - n-1 的全排列" << endl;
    print_permutation(n, A, 0);
    return 0;
}

說明一下,在上面上我們默認了P[] = {0, 1, 2, … , n-1}是沒有重複的元素的集合,所以就直接不用傳入P[]陣列了。
#
現在我們更進一步地討論, 如果P[4]= {1, 1,2,2}呢,這個是有多個重複的元素的集合。要是我們繼續使用遞迴構造的話,上面的放那個法就需要改進一下了。
我們需要考慮的問題如下:

  1. 當P陣列中存在重複陣列的時候,我們怎麼判斷函式已經歷遍了所有的元素,例如要生成排列 1122,A[0] = 1時,怎麼使A[1] = 1,A[2]=2, A[3]=2 ?

  2. 當P中存在重負元素是我們又怎麼避開多次使用相同元素作為跟節點展開?即 已經生成了排列以A[0] = 1為開頭的排列 1122, A[0] = 1為開頭的排列 1122 是不再允許被生成的。

對於問題1,我們可以在本次操作把P[i]插入A[cur]時候統計好P[i]在陣列A的 0 - cur-1 元素中 P[i]出現的次數 c2,和P[i] 在 陣列P中出現的個數c1。只要c1 > c2, 則P[i] 可以被繼續插入到 A中。
對於問題2, 我們事先對P陣列從小到大排序,對於本次需要最為根節點展開的元素 P[i] ,我們判斷 P[i] 是否與 P[i] 的上一個元素相等, 如果 P[i]=P[i-1] 的話則說明,對於P[i]相同的值在上一個同等高度的節點上已經使用過了,需要剪枝來避免重複列印。
生成可重複元素排列的程式碼如下:

//含有可重複的元素陣列的全排列

#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
void print_permutation(int n, int *P, int *A, int cur)
{
    if(n == cur)//排列到n個長度後輸出
    {
        for(int q = 0; q < n; q++)
            cout << A[q];
        cout << endl;
        return;
    }
    else
    {
        for(int i = 0; i < n; i++)
        {
            if(0 == i || P[i] != P[i-1])
            /*判斷是否當前使用的素是不是已經作為
            **在同一個集合裡拓展過的**/
            {
                int c1 = 0, c2 = 0;
                //判斷重複的元素是不是已經完全使用了
                for(int j = 0; j < n; j++)
                    if(P[i] == P[j])
                        c1++;
                for(int j = 0; j < cur; j++)
                    if(P[i] == A[j])
                        c2++;
                if(c1 > c2)
                {
                    A[cur] = P[i];
                    print_permutation(n,P,A,cur+1);
                    //遞迴
                }
            }
        }
    }
}
int main()
{
    int P[100], A[100] = {0};
    int n;
    cin >> n;
    for(int i = 0; i < n; i++)
        cin >> P[i];
    sort(P, P+n);
    cout << "可重複元素排列" << endl;
    print_permutation(5, P, A, 0);
    return 0;
}

生成全排列的方法也可以用STL裡的 next_permutation() 來實現用法如下:


#include<cstdio>
#include<string>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
    int P[100], A[100] = {0};
    int n;
    cin >> n;
    for(int i = 0; i < n; i++)
        cin >> P[i];
    sort(P, P+n);
    do{
        for(int i = 0; i < n; i++)
            cout << P[i];
        cout << endl;
    }while(next_permutation(P, p+n));

    return 0;
}

以上內容是個人基於對《演算法競賽入門經典第二版》暴力求解法部分章節的理解寫出來的,覺得大神們覺得有不當之處請多多指教。