康託展開是一個全排列到一個自然數的雙射,常用於構建哈希表時的空間壓縮。 康託展開的實質是計算當前排列在所有由小到大全排列中的順序,因此是可逆的。
今天訓練賽有一道全排列題,一開始用的是DFS,和next_permutation(全排列函數)的。結果沒想到教練卡了這兩個超時,同學0到9打表都能過,暈死!!!我去!
康託展開
其中, ai爲整數,並且 。
ai表示原數的第i位在當前未出現的元素中是排在第幾個
既然康託展開是一個雙射,那麼一定可以通過康託展開值求出原排列,即可以求出n的全排列中第x大排列。
康託展開逆運算
如n=5,x=96時:
首先用96-1得到95,說明x之前有95個排列.(將此數本身減去1)用95去除4! 得到3餘23,說明有3個數比第1位小,所以第一位是4.用23去除3! 得到3餘5,說明有3個數比第2位小,所以是4,但是4已出現過,因此是5.用5去除2!得到2餘1,類似地,這一位是3.用1去除1!得到1餘0,這一位是2.最後一位只能是1.所以這個數是45321。
按以上方法可以得出通用的算法。
逆康託展開舉例
一開始已經提過了,康託展開是一個全排列到一個自然數的雙射,因此是可逆的。即對於上述例子,在給出61可以算出起排列組合爲34152。由上述的計算過程可以容易的逆推回來,具體過程如下:
用 61 / 4! = 2餘13,等於2,說明比第一位小的數有2個,所以首位爲3。
拿 13 / 3! = 2餘1,說明在剩下的中,小於第二位的數有2個,但是所以第二位爲4。
拿 1 / 2! = 0餘1,說明在剩下的中,小於第三位的數,所以第三位爲1。
拿 1 / 1! = 1餘0,說明在剩下的中,小於第四位的數有1個,所以第四位爲5。
最後一位自然就是剩下的數2。
通過以上分析,所求排列組合爲 34152。
舉一反一(哈哈),題目給樣例:
5 10
得:
13452
分析下,先將10-1=9:
用9/4!(24) = 0餘9,意味着沒有數比第一位小: 1
拿9/3!(6) = 1餘3,意味着有一位數比第二位小,但是因爲1已經用過了。所以肯定是:3
拿3/2!(2) = 1餘1,意味着有一位數比第三位小,所以是:4
拿1/1!(1) = 1餘0,意味着沒有數比第四位小,所以是:5
剩餘2。
所以排列組合應該是:13452
代碼實現
#include<bits/stdc++.h>
using namespace std;
int n,m,sum;
static const int FAC[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}; // 存放階乘
//康託展開逆運算
int main(){
while(scanf("%d %d",&n,&m)!=EOF){
vector<int> v; // 存放當前可選數
vector<int> a; // 所求排列組合
int x=m-1;
for(int i=1;i<=n;i++)
v.push_back(i);
for(int i=n;i>=1;i--){
int r = x % FAC[i-1];
int t = x / FAC[i-1];
x = r;
sort(v.begin(),v.end());// 從小到大排序
a.push_back(v[t]); // 剩餘數裏第t+1個數爲當前位
v.erase(v.begin()+t); // 移除選做當前位的數
}
for(int i=0;i<a.size();i++){
printf("%d",a[i]);
}
printf("\n");
}
return 0;
}