康託展開(全排列)與逆運算

文章首發於

康託展開是一個全排列到一個自然數的雙射,常用於構建哈希表時的空間壓縮。 康託展開的實質是計算當前排列在所有由小到大全排列中的順序,因此是可逆的。

今天訓練賽有一道全排列題,一開始用的是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;
}

借鑑百度百科

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章