1. 程式人生 > 其它 >遞迴分治 --- 例題1.全排列

遞迴分治 --- 例題1.全排列

一.問題描述

設計一個遞迴演算法生成n個元素{r1, r2, … , rn}的全排列.
此題與力扣主站第46題 --- 全排列相同,以及力扣主站第47題 --- 全排列Ⅱ

二.解題思路

設R={r1,r2...,rn}是要進行排列的n個元素,Ri=R-{ri}.集合X中的元素的全排列記為Perm(X).(ri)Perm(X)表示在全排列X的每個排列前加上字首ri得到的排列.

R的全排列可歸納定義為如下:

  • 當n==1時,Perm(R)=(r),其中r是集合R中唯一的元素.
  • 當n > 1時,Perm(R)由(r1)Perm(R1),(r2)Perm(r2),...,(rn)Perm(rn)構成

通過上述分析,我們可以很清晰地看到,規模較大的問題可以劃分為規模較小的問題來,並且總問題的解可以由每個小問題的解合併而來.
有了這個性質,那麼我們很自然地就可以聯絡上遞迴分治思想.

程式碼如下:

void Perm(T list[], int low, int high)
{
    if(low == high)		//表示已經填到最後一個字元,輸出答案
    {
        for(int i=0; i<=high; ++i)
        {
            cout<<list[i];
            cout.width(4);
        }
        cout<<'\n';
    }
    for(int i=low; i<=high; ++i)  //0~low為已經填好的字元,我們在low~high中選擇新字元填入,即將list[i]與list[low]交換位置
    {
        swap(list[i], list[low]);
        Perm(list, low+1, high);  //繼續遞迴填寫下一個字元,引數low變為low+1
        swap(list[i], list[low]); //遞歸回來之後記得交換回來,恢復原樣
    }
}

下面我們更深一步,考慮一下如果有重複元素在陣列中應該怎麼辦?

由於全排列就是從第一個數字起每個數分別與它後面的數字交換.我們先嚐試加個這樣的判斷——如果一個數與後面的數字相同那麼這二個數就不交換了.
如122,第一個數與後面交換得212、221.然後122中第二數就不用與第三個數交換了,但對212,它第二個數與第三個數是不相同的,交換之後得到221,與由122中第一個數與第三個數交換所得的221重複了.所以這個方法不行.

換種思維,對122,第一個數1與第二個數2交換得到212,然後考慮第一個數1與第三個數2交換,

此時由於第三個數等於第二個數,所以第一個數不再與第三個數交換.再考慮212,它的第二個數與第三個數交換可以得到解決221.此時全排列生成完畢.

這樣我們也得到了在全排列中去掉重複的規則——去重的全排列就是從第一個數字起每個數分別與它後面非重複出現的數字交換.用程式設計的話描述就是第i個數與第j個數交換時,要求[i,j)中沒有與第j個數相等的數.即保證每次填入的數一定是這個數所在重複數集合中「從左往右第一個未被填過的數.
判斷在str陣列中,[nBegin,nEnd]中是否有數字與下標為nEnd的數字相等

程式碼如下:

#include<bits/stdc++.h>
using namespace std;
bool IsSwap(char *str, int nBegin, int nEnd)  //保證每次填入的數一定是這個數所在重複數集合中「從左往右第一個未被填過的數字」
{
    for(int i=nBegin; i<nEnd; i++)
        if(str[i]==str[nEnd]) return false;
    return true;
}
// k表示當前選取到第幾個數,m表示陣列大小
void AllRange(char *str, int k, int m)
{
    if(k==m)
    {
        static int count = 1;
        string ans = "";
        for(int i=0; i<m; i++) ans += str[i];
        cout<<"第"<<count++<<"個排列是"<<ans<<endl;
    }
    else 
    {
        for(int i=k; i<m; i++)
        {
            if(IsSwap(str, k, i))  //同一層中相同的元素只有第一個有用
            {
                swap(str[i], str[k]);
                AllRange(str, k+1, m);
                swap(str[i], str[k]);
            }
        }
    }
}
int main()
{
    // cout<<"陣列中沒有重複元素的全排列:"<<endl;
    // int n;
    // cout<<"請輸入陣列大小:";
    // while(cin>>n && n!=0)
    // {
    //     cout<<"請輸入陣列元素:"<<endl;
    //     int *a = new int[n];
    //     for(int i=0; i<n; i++) cin>>a[i];
    //     Perm(a, 0, n);
    //     cout<<"請輸入陣列大小:";
    // }

    cout<<"陣列中有重複元素的全排列:"<<endl;
    int n;
    cout<<"請輸入陣列大小:";
    while(cin>>n && n!=0)
    {
        cout<<"請輸入陣列元素:"<<endl;
        char *a = new char[n];
        for(int i=0; i<n; i++)  cin>>a[i];
        AllRange(a, 0, n);
        cout<<"請輸入陣列大小:";
    }
    system("pause");
    return 0;
}

執行結果:(第一幅圖為沒有重複元素的全排列,第二幅圖為有重複元素的全排列(資料太猛了,13個字元的全排列跑了很久))

參考畢方明老師《演算法設計與分析》課件.

如果覺得本篇文章對你有所幫助,歡迎大家來到我的個人部落格網站---喬治的程式設計小屋逛一逛吧.