康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
公式
其中, 为整数,并且
康托展开简单点说就是,判断这个数在其各个数字全排列中从小到大排第几位。
比如 132,在1、2、3的全排列中排第2位。
康托展开求法:
比如2143 这个数:
从头开始判断,
① 比 2(第一位数)小的数有多少个-> 1个就是1,1*3!
② 比 1(第二位数)小的数有多少个-> 0个0*2!
③ 比 4(第三位数)小的数有多少个-> 3个就是1,2,3,但是1,2之前已经出现,所以是 1*1!
X=1*3!+0*2!+1*1!+0*0! = 7
比该数小的数有7个,所以该数排第8的位置。
1234 1243 1324 1342 1423 1432 2134 2143 2314 2341 2413 …(略)
既然是双射,那么也可以反推求出2143:
① 首先我们需要推出a序列
7 / 3! = 1 所以 a[3] = 1 , 7 % 3! = 1,
1 / 2! = 0 所以 a[2] = 0 , 1 % 2! = 1,
1 / 1! = 1 所以 a[1] = 1 , 1 % 1! =0,
同理a[0] = 0.
所以得到a数组为1 0 1 0② 再由a数组推出序列,根据a数组的意义反推。
a[3] = 1, 表示它在1 2 3 4 序列中比它小的有1个,即它自己排第2,它等于2
a[2] = 0, 表示它在1 3 4序列中比它小的有0个,即他是最小的,它等于1
a[1] = 1, 表示它在3 4序列中比它小的有1个,即它自己排第2,它等于4
a[0] = 0, 表示它在3中最小,只能是3
因此序列为2 1 4 3
应用
一个直观的应用就是给定一个自然数集合和一个序列,求这个序列中按从小到大的顺序排第几个?
比如对于1 2 3 4 5, 序列3 1 2 5 4比它小的序列有49个,即它排第50
另一个应用就是逆推第K个排列是多少,比如第50个全排列是多少?则首先减1得到49, 再反推即可得到3 1 2 5 4
另外在保存一个序列,我们可能需要开一个数组,如果能够把它映射成一个自然数, 则只需要保存一个整数,大大压缩空间.
计算
编码
static const int Factorial[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
int cantor(int *a, int n)
{
assert(n < 10);
int ans = 0;
for (int i = 0; i < n; ++i) {
int small = 0;
for (int j = i + 1; j < n; ++j) {
if (a[j] < a[i])
small++;
}
ans += Factorial[n - i - 1] * small;
}
return ans;
}
解码
void decantor(int *a, int n, int k)
{
//由于使用set容器,所以序列不能有重复,否则要换容器
set<int> num;
for(int i=1;i<=n;i++)
num.insert(i);
int cur = 0;
for (int i = n - 1; i > 0; --i) {
int index = k / Factorial[i];
k %= Factorial[i];
set<int>::iterator it=next(num.begin(),index);
a[cur++] = *it ;
num.erase(*it);
}
set<int>::iterator it=num.begin();
a[cur] = *it;
num.clear();
}
test:
int main()
{
int n,number[20],denumber[20];
cin>>n;
for(int i=0;i<n;i++){
cin>>number[i];
}
int ans=cantor(number,n);
cout<<ans<<endl;
decantor(denumber,n,ans);
for(int i=0;i<n;i++){
cout<<denumber[i]<<" ";
}
cout<<endl;
return 0;
}