1. 程式人生 > >全排列問題演算法及實現(Permutation)

全排列問題演算法及實現(Permutation)

前言

做專案遇到資料採集系統中ADC拼合問題,如果順序不對,波形就是錯誤的(題外話),為了找到正確的順序,涉及到排列問題。

什麼是排列組合

定義

一般地,從n個不同元素中取出m(m≤n)個元素,按照一定的順序排成一列,叫做從n個元素中取出m個元素的一個排列(Arrangement)。特別地,當m=n時,這個排列被稱作全排列(Permutation)。 
Ex:考慮三個數字1,2,3,這個序列有6個可能的排列組合 
123 
132 
213 
231 
312 
321 
這些排列組合根據less-than操作符做字典順序的排序。 
字典順序顧名思義是就是將1-n的一個排列看成一個數,然後按照字典的順序從小到達的輸出

全排列及序號

所謂的全排列,就是說將數字進行不重複的排列,所有得到的序列,就是全排列 
給定數字1 , 2 , 3 , 4,其全排列是: 
{1,2,3,4}, {1,2,4,3}, {1,3,2,4}, {1,3,4,2}, {1,4,2,3}, {1,4,3,2}

{2,1,3,4}, {2,1,4,3}, {2,3,1,4}, {2,3,4,1}, {2,4,1,3}, {2,4,3,1}

{3,1,2,4}, {3,1,4,2}, {3,2,1,4}, {3,2,4,1}, {3,4,1,2}, {3,4,2,1}

{4,1,2,3}, {4,1,3,2}, {4,2,1,3}, {4,2,3,1}, {4,3,1,2}, {4,3,2,1} 
全排列如上所示,那麼什麼是全排列的序號?這裡我們通常將全排列按照字典序進行編排,就如上面從左到右看,就是按照字典序排列的。 
我們說,對於1,2,3,4的全排列,第20號序列是{4,1,3,2},因為其在這個按照字典序排列的全排列中處在第20的位置。

排列組合涉及的問題

  • 下一個全排列
  • 上一個全排列
  • 給定一個排列的序號以及排列中數字的個數,那麼這個排列是什麼
  • 給定一個排列,求這個排列的序號是多少

下一個全排列(next_permutation)

在STL中,有next_permutation的演算法實現。 
next_permutation()會取得[first,last) 所標之序列的下一個排列組合。如果沒有下一個排列組合,便返回false,否則返回true。 
演算法: 
首先,從最尾端開始往前尋找兩個相鄰元素,令第一個元素為*i,第二個元素為*ii,且滿足*i < *ii。找到這樣一組相鄰元素後,再從最尾端開始往前檢驗,找出第一個大於*i 的元素,令為*j ,將i,j元素對調,再將ii之後的所有元素顛倒排序。 
如下圖:方框為i和ii 

 
Code:

class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        vector<int>::iterator first=nums.begin();
        vector<int>::iterator last=nums.end();
        if(first==last)      //empty
            return;
        vector<int>::iterator i=first;
        i++;
        if(i==last)    //only one element
            return;
        i=last;        //i指向尾端
        i--;           
        for(;;)
        {
            vector<int>::iterator ii=i;
            --i;   //鎖定一組(兩個)相鄰元素
            if(*i<*ii)   //如果前一個元素小於後一個元素
            {
                vector<int>::iterator j=last;    //j指向尾端
                while(!(*i < *--j));             //從尾端往前找,直到比*i大的元素
                iter_swap(i,j);
                reverse(ii,last);
                return;
            }
            if(i==first)
            {
                reverse(first,last);
                return;
            }
        }
    }
};

上一個全排列(prev_permutation)

首先,從最尾端開始往前尋找兩個相鄰元素,令第一個元素為*i,第二個元素為*ii,且滿足*i > *ii。找到這樣一組相鄰元素後,再從最尾端開始往前檢驗,找出第一個小於*i 的元素,令為*j ,將i,j元素對調,再將ii之後的所有元素顛倒排序。 
程式碼和next_permutation類似

已知排列求序號

康託展開式實現了由1到n組成的全排列序列到其編號之間的一種對映 
公式: 
X=an*(n-1)!+an-1*(n-2)!+…+ai*(i-1)!+…+a2*1!+a1*0!

由1到n這n個數組成的全排列,共n!個,按每個全排列組成的數從小到大進行排列,並對每個序列進行編號(從0開始),並記為X;比如說1到4組成的全排列中,1234對應編號0,1243對應編號1。

對於ai(係數)的解釋需要用例子來說明:

對1到4的全排列中,我們來考察3214,則

  1. a4={3在集合(3,2,1,4)中是第幾大的元素,有多少個逆序對}=2

  2. a3={2在集合(2,1,4)中是第幾大的元素,有多少個逆序對}=1

  3. a2={1在集合(1,4)中是第幾大的元素,有多少個逆序對}=0

  4. a1=0(最後只剩下一項)

也就是說康託公式中的每一項依次對應全排列序列中的每個元素,並按上述規則對映;

則X=2*3!+1*2!+0*1!+0*0!=14,即3214對應的編號為14。

Code:

//已知排列求序號
long long getIndex(short dim, short *rankBuf)
{
    int i, j;
    long long index = 0;
    long long k = 1;
    for (i = dim - 2; i >= 0; k *= dim - (i--))//每一輪後k=(n-i)!,注意下標從0開始
        for (j = i + 1; j<dim; j++)
            if (rankBuf[j]<rankBuf[i])
                index += k;//是否有逆序,如有,統計,即計算係數
    return index;
}

已知序號求排列

康託公式可以根據排列的序號來求出該排列,即通過X的值求出ai(i大於等於1小於等於n)的值,運用輾轉相除的方法即可實現,現在已知一個編號14(注意該編號是從0開始的,如果是從1開始,此處要減1),求其對應的全排列序列: 
14 / (3!) = 2 餘 2 
2 / (2!) = 1 餘 0 
0 / (1!) = 0 餘 0 
0 / (0!) = 0 餘 0 
故得到:a4=2,a3=1,a2=0,a1=0,由ai的定義即可確定14對應的全排列為2103.
Code:

//已知序號求排列
void getPermutation(int dim, short *rankBuf, long long index){
    short i, j; 
    //求逆序數陣列
    for (i = dim - 1; i >= 0; i--)
    {
        rankBuf[i] = index % (dim - i);
        index /= dim - i;
    }
    for (i = dim - 1; i; i--)
        for (j = i - 1; j >= 0; j--)
            if (rankBuf[j] <= rankBuf[i])
                rankBuf[i]++;       //根據逆序數陣列進行調整   
}