康托展开
一、康托展开
1. 定义
康托展开是一个全排列到一个自然数的双映射,常用于构建哈希表时的空间压缩;
设有 nnn 个数 (1,2,3,4,...,n)(1, 2, 3, 4, ... , n)(1,2,3,4,...,n) ,可以组成 n!n!n! 种不同的排列组合,康托展开表示的就是当前排列组合再所有全排列中的字典序名次;
康托展开的实质是计算当前排列在所有的由小到大全排列中的顺序,因此是可逆的;
2. 原理
设有排列 p=a1a2...anp = a_1 a_2 ... a_np=a1a2...an ,则对于字典序比 ppp 小的排列一定存在排列 p1p_1p1 ,使得排列的前 i−1i - 1i−1 位与 ppp 相同,第 iii 位比 pip_ipi 小,后续位随意;
对于任意 iii ,满足条件的排列数就是从 n−i+1n - i + 1n−i+1 位中选一个比 aia_iai 小的数,并将剩下的 n−in - in−i 个数随意排列,则方案数为 Ai∗(n−i)!A_i * (n - i)!Ai∗(n−i)! (其中 AiA_iAi 表示 aia_iai 后面比 aia_iai 小的数的个数);
则总的方案数即为 Σi=1n−1Ai∗(n−i)!\Sigma_{i = 1}^{n - 1} A_i * (n - i)!Σi=1n−1Ai∗(n−i)! ,再加 1 即为排名;
关于求 AiA_iAi ,可以用 O(n2)O(n^2)O(n2) 求,也可以用树状数组优化到 O(nlogn)O(nlog_n)O(nlogn) ;
3. 例子
若 p=4,1,3,2p = 4, 1, 3, 2p=4,1,3,2 ,可以求得 A=[3,0,1,0]A = [3, 0, 1, 0]A=[3,0,1,0] ,则;
第一位比 p1p_1p1 小的排列数为 3∗3!=183 * 3! = 183∗3!=18 ;
第一位与 p1p_1p1 相等,第二位比 p2p_2p2 小的排列数为 000 ;
第一,二位分别与 p1,p2p_1, p_2p1,p2 相等,第三位比 p3p_3p3 小的排列数为 1∗1!=11 * 1! = 11∗1!=1 ;
所以 ppp 的排名是 18+0+1+1=2018 + 0 + 1 + 1 = 2018+0+1+1=20 ;
4. 代码
预处理
预处理出阶乘与 AAA 数组
O(n2)O(n^2)O(n2) 写法
void firstset(int n) {
fc[1] = 1;
for (int i = 2; i <= n; i++) {
fc[i] = (fc[i - 1] * i);
}
for (int i = 1; i <= n; i++) {
for (int j = i + 1; j <= n; j++) {
if (a[j] < a[i]) {
b[i]++;
}
}
}
return;
}
树状数组写法
int lowbit(int x) {
return x & (-x);
}
void add(int x, int a) {
while (x <= n) {
c[x] += a;
x += lowbit(x);
}
return;
}
int query(int x) {
int ans = 0;
while (x != 0) {
ans += c[x];
x -= lowbit(x);
}
return ans;
}
void firstset(int n, int a[]) {
fc[1] = 1;
for (int i = 2; i <= n; i++) {
fc[i] = (fc[i - 1] * i);
}
for (int i = n; i >= 1; i--) {
b[i] = query(a[i]); // 前缀和查询,查询比 a[i] 小的数字数量
add(a[i], 1); // 单点修改
}
return;
}
康托展开
int cantor(int n, int a[]) {
int ans = 1;
for (int i = 1; i < n; i++) {
ans += b[i] * fc[n - i];
}
return ans;
}
三、逆康托展开
1. 定义
逆康托展开即为指定排名求排列;
2. 原理
n!=n∗(n−1)!=(n−1)∗(n−1)!+(n−1)!n! = n * (n - 1)! = (n - 1) * (n - 1)! + (n - 1)!n!=n∗(n−1)!=(n−1)∗(n−1)!+(n−1)!
继续展开得
n!=Σi=1n−1(i∗i!)+1n! = \Sigma_{i = 1}^{n - 1}(i * i!) + 1n!=Σi=1n−1(i∗i!)+1
则
n!>Σi=1n−1(i∗i!)n! > \Sigma_{i = 1}^{n - 1}(i * i!)n!>Σi=1n−1(i∗i!)
n!>(n−1)∗(n−1)!+(n−2)∗(n−2)!+...+2∗2!+1∗1!n! > (n - 1) * (n - 1)! + (n - 2) * (n - 2)! + ... + 2 * 2! + 1 * 1!n!>(n−1)∗(n−1)!+(n−2)∗(n−2)!+...+2∗2!+1∗1!
由于每一项的 (n−i)!(n - i)!(n−i)! 都比后面所有项的总和还大,则可以用类似进制转化的方法,不断除模得到得到 AAA 数组;
得到 AAA 数组后,即可得到 pip_ipi 就是剩余未用的数中第 Ai+1A_i + 1Ai+1 小的;
3. 例子
以 1,2,3,4,5{1, 2, 3, 4, 5}1,2,3,4,5 的排列为例,求第 96 个排列;
首先 96−1=9596 - 1 = 9596−1=95 ;
95/4!=3......2395 / 4! = 3 ...... 2395/4!=3......23 ,可知比它小的数有 3 个,即为 4 ;
23/3!=3......523 / 3! = 3 ...... 523/3!=3......5 ,可知比它小的数有 2 个,因为 4 已经出现过,所以为 5 ;
5/2!=2......15 / 2! = 2 ...... 15/2!=2......1 ,可知比它小的数有 2 个,即为 3 ;
1/1!=11 / 1! = 11/1!=1 ,可知只有 2 符合;
最后一位就是剩下的 1 ;
最终结果为 45321 ;
4. 代码
预处理
预处理出阶乘
void firstset(int n) {
fc[0] = fc[1] = 1;
for (int i = 2; i <= n; i++) {
fc[i] = i * fc[i - 1];
}
return;
}
逆康托展开
void inverse_cantor(int m, int n) { // m 为排名,n 为排列的长度
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 % fc[i - 1];
int t = x / fc[i - 1];
x = r;
a.push_back(v[t]); // 剩余数里的第 t + 1 个数
v.erase(v.begin() + t); // 移除选做当前位的数
}
for (int i = 0; i < a.size(); i++) {
printf("%d", a[i]);
}
return;
}
954

被折叠的 条评论
为什么被折叠?



