康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
以下称第x个全排列是都是指由小到大的顺序。
公式:
X=a[n]*(n-1)!+a[n-1]*(n-2)!+…+a[i]*(i-1)!+…+a[1]*0!
其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。
公式看起来可能不太明白,其实很简单。。
举个例子:
对abcdefghijkl(12个字符)进行字典序全排列(共有12!种排列),现在给你一个排列,让你求它是第几个排列(即展开)。比如给gfkedhjblcia,展开即为:X = 6*11!+ 5*10! + 8*9! + 4*8! + 3*7! + 3*6! + 4*5! + 1*4! + 3*3! + 1*2! + 1*1! + 0*0! = 260726925 (这答案是在排在他前面的序列数,他自己的位次应该是这个+1).
再看公式:X=a[n]*(n-1)!+a[n-1]*(n-2)!+…+a[i]*(i-1)!+…+a[1]*0!
a[n]是序列第n个数(从右往左数)在他之前(即在它右边)比它小的数的数量,像例子中a[12] = 6, 第12个数是g, 它右边比它小的数有a, b, c, d, e, f. 之后的a[11], a[10]....a[1]同理。
原理:
知道原理就很简单了,也不用死记硬背了,要计算这个序列是全排列的第几个,可以通过计算它前面有几个,再加一就是它是第几个。
从最高位开始算(最左边),这位比它小的,后面几位无论怎么排,序列肯定比它小,所以找出能放在这一位(在它前面不出现)且比它小的个数再乘上后面位数阶乘就行了。这一位相同的情况下,下一位同理。
还是没看懂的话看看这两个举例吧,
举例:
康托展开的逆运算:
既然康托展开是一个双射,那么一定可以通过康托展开值求出原排列,即可以求出n的全排列中第x大排列。
就是上面的反过来计算,自己思考吧。
康托展开的代码:
题目地址:https://qduoj.com/problem/26/
/*
样例输入1 复制
abcdefghijkl
abcdefghiklj
gfkedhjblcia
样例输出1
1
4
260726926
*/
#include<iostream>
#include<cstdio>
using namespace std;
int main(void)
{
long long fc[13] = {1};
//计算阶乘
for(int i = 1; i < 13; i++) fc[i] = fc[i-1] * i;
char str[20];
while(~scanf("%s", str))
{
long long ans = 1;
for(int i = 0; i < 12; i++)
{
int count = 0;
//找它后面比它小的数数量
for(int j = i; j < 12; j++)
if(str[j] < str[i]) count++;
printf(" %d ", count);
ans += count * fc[11-i];
}
printf("%lld\n", ans);
}
return 0;
}