康托展开
简介
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。康托展开的实质是计算当前排列在所有由小到大的排列中的顺序,因此是可逆的。——百度百科
简单来说,通过康托展开可以确定一个给定的 \(n\) 位数的排列是字典序中的第几个排列(排名)
康托展开
给定一个 \(n\) 位数的排列 \(a_1,a_2\cdots a_n\) ,则康托展开值 \(k\) 有如下公式:
其中 \(A_i=\sum_{j=i+1}^n [a_j<a_i]\) ,表示在原排列中排在 \(a_i\) 后且比 \(a_i\) 小的数的个数
以 \(4,1,5,3,2\) 为例:
- 排在 \(4\) 后且小于 \(4\) 的有 \(3\) 个,所以 \(x=1+3\times4!\)
- 排在 \(1\) 后且小于 \(1\) 的有 \(0\) 个,所以 \(x=1+72+0\)
- 排在 \(5\) 后且小于 \(5\) 的有 \(2\) 个,所以 \(x=1+72+0+2\times2!\)
- 排在 \(3\) 后且小于 \(3\) 的有 \(1\) 个,所以 \(x=1+72+0+4+1\times1!\)
以此类推,可以算出此排列的排名是 \(78\)
这样做的原因是:排在 \(a_i\) 后且比 \(a_i\) 小的数都可以放到 \(a_i\) 的位置,并且这样以后第 \(i+1\sim n\) 位上的数任意排列,得到的新排列排名都小于原排列。根据乘法原理,新排列的个数为 \(A_i\times(n-i)!\)
暴力:
int ans = 1;
int fact[0] = 1;
for(int i = 1; i <= n; i++)
fact[i] = fact[i - 1] * i;
for(int i = 1; i <= n; i++) {
int cnt = 0;
for(int j = i + 1; j <= n; j++)
if(a[j] < a[i])
cnt++;
ans += cnt * fact[n - i];
}
时间复杂度 \(O(n^2)\)
优化:
可以发现,暴力做法的查找效率较低,那么如何优化呢?可以考虑用树状数组
我们建立一个数组 \(c\) ,当遍历到 \(a_i\) 时,就把 \(c[a[i]]\) 设为 \(true\) ,那么数组 \(c\) 的前缀和 \(sum[a[i]-1]\) 就表示在 \(a_i\) 前面且小于 \(a_i\) 的数的个数,那么在 \(a_i\) 后且比 \(a_i\) 小的数的个数即为 \(a[i]-1-sum[a[i]-1]\) ,这样就可以用树状数组维护了
#include<bits/stdc++.h>
using namespace std;
const int MOD = 998244353;
const int MAX_N = 1000000 + 5;
int n;
int a[MAX_N];
int c[MAX_N];
int fact[MAX_N];
int lowbit(int x)
{
return x & (-x);
}
void update(int x, int k)
{
while(x <= n) {
c[x] += k;
x += lowbit(x);
}
}
int get_sum(int x)
{
int res = 0;
while(x >= 1) {
res += c[x];
x -= lowbit(x);
}
return res;
}
int main()
{
scanf("%d", &n);
fact[0] = 1;
for(int i = 1; i <= n; i++)
fact[i] = 1ll * fact[i - 1] * i % MOD;
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]);
int ans = 1;
for(int i = 1; i <= n; i++) {
update(a[i], 1);
ans = (ans + 1ll * fact[n - i] * (a[i] - get_sum(a[i] - 1) - 1) % MOD) % MOD;
}
printf("%d\n", ans);
return 0;
}
时间复杂度 \(O(n\log n)\)
逆康托展开
康托展开的逆运算,求的是在 \(1\sim n\) 的全排列中排名第 \(k\) 的排列
以 \(n=5,k=78\) 为例:
-
\(k-1=77\)
-
\(77\div 4!=3\cdots5\) ,比第一个数小且没出现过的数有 \(3\) 个, \(a[1]=4\)
-
\(5\div 3!=0\cdots5\) ,比第二个数小且没出现过的数有 \(0\) 个, \(a[2]=1\)
-
\(5\div 2!=2\cdots1\) ,比第三个数小且没出现过的数有 \(2\) 个, \(a[3]=5\)
-
\(1\div 1!=1\cdots 0\) ,比第四个数小且没出现过的数有 \(1\) 个, \(a[4]=3\)
所以所求排列为 \(4,1,5,3,2\)
#include<bits/stdc++.h>
using namespace std;
int n, k;
int res[15], fact[15];
bool vis[15];
int main()
{
scanf("%d%d", &n, &k);
fact[0] = 1;
for(int i = 1; i <= n; i++)
fact[i] = fact[i - 1] * i;
k--;
int t, r = k;
for(int i = 1; i <= n; i++) {
t = r / (fact[n - i]);
r = r % fact[n - i];
for(int j = 1; j <= n; j++) {
if(!vis[j]) {
if(t == 0) {
vis[j] = true;
res[i] = j;
break;
}
t--;
}
}
}
for(int i = 1; i <= n; i++)
printf("%d\n", res[i]);
return 0;
}