-
題面描述
現有一個長度爲n的集合S,集合S裏的元素爲 [1,2,3……n]。按照高中數學的知識,我們顯然可以知道,集合裏的元素的排列共有 n! 種。現在給你集合長度n以及一個正整數 Q,請你找出集合元素組成的第Q個的排列。 -
輸入
輸入數據由多組測試樣例組成,每組測試樣例第一行分別輸入兩個正整數n ( 1 <= n <= 9 ),Q( 1 <= Q <= n! ) -
輸出
輸出第Q個的排列,元素之間沒有空格 -
樣例輸入
2 2
5 10
- 樣例輸出
21
13452
第一發超時代碼是暴力dfs找第Q個排列, 毫無疑問的超時了,這時我想起 next_permutation() 函數,於是有了以下代碼:
#include <bits/stdc++.h>
using namespace std;
int k[15];
int main()
{
int n, m;
while(~scanf("%d%d", &n, &m))
{
for(int i = 0; i < n; i++)
{
k[i] = i + 1;
}
int t = 1;
do
{
if(t == m)
{
for(int i = 0; i < n; i++)
printf("%d", k[i]);
printf("\n");
break;
}
t++;
}while(next_permutation(k, k + n));
}
return 0;
}
沒想到出題人早就料到了這種情況,又超時了,無奈下上網查到了康拓展開/逆康拓展開。
簡述
康託展開是一個全排列到一個自然數的雙射,常用於構建hash表時的空間壓縮。設有n個數(1,2,3,4,…,n),可以有組成不同(n!種)的排列組合,康託展開表示的就是是當前排列組合在n個不同元素的全排列中的名次。
原理
X=a[n] * (n-1)!+a[n-1] * (n-2)!+…+a[i] * (i-1)!+…+a[1]*0!
排列組合 | 名次 | 康託展開 |
---|---|---|
123 | 1 | 0 * 2! + 0 * 1! + 0 * 0! |
132 | 2 | 0 * 2! + 1 * 1! + 0 * 0! |
213 | 3 | 1 * 2! + 0 * 1! + 0 * 0! |
231 | 4 | 1 * 2! + 1 * 1! + 0 * 0! |
312 | 5 | 2 * 2! + 0 * 1! + 0 * 0! |
321 | 6 | 2 * 2! + 1 * 1! + 0 * 0! |
舉個例子
在(1,2,3,4,5)5個數的排列組合中,計算 34152的康託展開值。
- 首位是3,則小於3的數有兩個,爲1和2,a[5]=2,則首位小於3的所有排列組合爲 a[5]*(5-1)!
- 第二位是4,則小於4的數有兩個,爲1和2,注意這裏3並不能算,因爲3已經在第一位,所以其實計算的是在第二位之後小於4的個數。因此a[4]=2
- 第三位是1,則在其之後小於1的數有0個,所以a[3]=0
- 第四位是5,則在其之後小於5的數有1個,爲2,所以a[2]=1
- 最後一位不用計算,因爲在它之後已經沒有數了,所以a[1]固定爲0
- 根據公式:
X = 2 * 4! + 2 * 3! + 0 * 2! + 1 * 1! + 0 * 0! = 2 * 24 + 2 * 6 + 1 = 61
所以比 34152 小的組合有61個,即34152是排第62。
具體代碼實現如下:(假設排列數小於10個)
int k[10] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}; //階乘
int cantor(int *a, int q)
{
int x = 0;
for(int i = 0; i < n; i++)
{
int smaller = 0; // 在當前位之後小於其的個數
for(int j = i + 1; j < n; j++)
{
if(a[j] < a[i])
smaller++;
}
x += k[n - i - 1] * smaller; // 康託展開累加
}
return x; // 康託展開值
}
逆康託展開
康託展開是一個全排列到一個自然數的雙射,因此是可逆的。即對於上述例子,在(1,2,3,4,5)給出61可以算出起排列組合爲 34152。由上述的計算過程可以容易的逆推回來,具體過程如下:
- 用 61 / 4! = 2餘13,說明a[5]=2,說明比首位小的數有2個,所以首位爲3。
- 用 13 / 3! = 2餘1,說明a[4]=2,說明在第二位之後小於第二位的數有2個,所以第二位爲4。
- 用 1 / 2! = 0餘1,說明a[3]=0,說明在第三位之後沒有小於第三位的數,所以第三位爲1。
- 用 1 / 1! = 1餘0,說明a[2]=1,說明在第二位之後小於第四位的數有1個,所以第四位爲5。
- 最後一位自然就是剩下的數2。
- 通過以上分析,所求排列組合爲 34152。
具體代碼實現如下:(假設排列數小於10個)
int k[15] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880}; // 階乘
//康託展開逆運算
void decantor(int x, int n)
{
vector <int> ve; // 存放當前可選數
vector <int> a; // 所求排列組合
for(int i = 1; i <= n; i++)
ve.push_back(i);
for(int i = n; i >= 1; i--)
{
int r = x % k[i - 1];
int t = x / k[i - 1];
x = r;
sort(ve.begin(), ve.end()); // 從小到大排序
a.push_back(ve[t]); // 剩餘數裏第t+1個數爲當前位
ve.erase(ve.begin() + t); // 移除選做當前位的數
}
}
那麼看懂了康拓/逆康拓展開,也就能輕(shi)輕(fen)鬆(fei)鬆(li)的解出這道題,以下爲ac代碼:
#include <bits/stdc++.h>
using namespace std;
int k[15] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
void decantor(int x, int n)
{
vector <int> ve;
vector <int> a;
for(int i = 1; i <= n; i++)
ve.push_back(i);
for(int i = n; i >= 1; i--)
{
int r = x % k[i - 1];
int t = x / k[i - 1];
x = r;
sort(ve.begin(), ve.end());
a.push_back(ve[t]);
ve.erase(ve.begin() + t);
}
for(int i = 0; i < n; i++)
printf("%d", a[i]);
printf("\n");
}
int main()
{
int n, m;
while(~scanf("%d%d", &n, &m))
{
decantor(m - 1, n); // 注意這裏m-1纔是康拓值
}
return 0;
}
應用
- 給定一個自然數集合組合一個全排列,所其中的一個排列組合在全排列中從小到大排第幾位。
在上述例子中,在(1,2,3,4,5)的全排列中,34152的排列組合排在第62位。 - 反過來,就是逆康託展開,求在一個全排列中,從小到大的第n個全排列是多少。
比如求在(1,2,3,4,5)的全排列中,第62個排列組合是34152。[注意具體計算中,要先 -1 纔是其康託展開的值。 - 另外康託展開也是一個數組到一個數的映射,因此也是可用於hash,用於空間壓縮。比如在保存一個序列,我們可能需要開一個數組,如果能夠把它映射成一個自然數, 則只需要保存一個整數,大大壓縮空間。比如八數碼問題。