全排列問題演算法及實現(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,則
-
a4={3在集合(3,2,1,4)中是第幾大的元素,有多少個逆序對}=2
-
a3={2在集合(2,1,4)中是第幾大的元素,有多少個逆序對}=1
-
a2={1在集合(1,4)中是第幾大的元素,有多少個逆序對}=0
-
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]++; //根據逆序數陣列進行調整
}