康托展开
一、康托展开
1. 定义
康托展开是一个全排列到一个自然数的双映射,常用于构建哈希表时的空间压缩;
设有 n n n 个数 ( 1 , 2 , 3 , 4 , . . . , n ) (1, 2, 3, 4, ... , n) (1,2,3,4,...,n) ,可以组成 n ! n! n! 种不同的排列组合,康托展开表示的就是当前排列组合再所有全排列中的字典序名次;
康托展开的实质是计算当前排列在所有的由小到大全排列中的顺序,因此是可逆的;
2. 原理
设有排列 p = a 1 a 2 . . . a n p = a_1 a_2 ... a_n p=a1a2...an ,则对于字典序比 p p p 小的排列一定存在排列 p 1 p_1 p1 ,使得排列的前 i − 1 i - 1 i−1 位与 p p p 相同,第 i i i 位比 p i p_i pi 小,后续位随意;
对于任意 i i i ,满足条件的排列数就是从 n − i + 1 n - i + 1 n−i+1 位中选一个比 a i a_i ai 小的数,并将剩下的 n − i n - i n−i 个数随意排列,则方案数为 A i ∗ ( n − i ) ! A_i * (n - i)! Ai∗(n−i)! (其中 A i A_i Ai 表示 a i a_i ai 后面比 a i a_i ai 小的数的个数);
则总的方案数即为 Σ i = 1 n − 1 A i ∗ ( n − i ) ! \Sigma_{i = 1}^{n - 1} A_i * (n - i)! Σi=1n−1Ai∗(n−i)! ,再加 1 即为排名;
关于求 A i A_i Ai ,可以用 O ( n 2 ) O(n^2) O(n2) 求,也可以用树状数组优化到 O ( n l o g n ) O(nlog_n) O(nlogn) ;
3. 例子
若 p = 4 , 1 , 3 , 2 p = 4, 1, 3, 2 p=4,1,3,2 ,可以求得 A = [ 3 , 0 , 1 , 0 ] A = [3, 0, 1, 0] A=[3,0,1,0] ,则;
第一位比 p 1 p_1 p1 小的排列数为 3 ∗ 3 ! = 18 3 * 3! = 18 3∗3!=18 ;
第一位与 p 1 p_1 p1 相等,第二位比 p 2 p_2 p2 小的排列数为 0 0 0 ;
第一,二位分别与 p 1 , p 2 p_1, p_2 p1,p2 相等,第三位比 p 3 p_3 p3 小的排列数为 1 ∗ 1 ! = 1 1 * 1! = 1 1∗1!=1 ;
所以 p p p 的排名是 18 + 0 + 1 + 1 = 20 18 + 0 + 1 + 1 = 20 18+0+1+1=20 ;
4. 代码
预处理
预处理出阶乘与 A A A 数组
O ( n 2 ) 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 = 1 n − 1 ( i ∗ i ! ) + 1 n! = \Sigma_{i = 1}^{n - 1}(i * i!) + 1 n!=Σi=1n−1(i∗i!)+1
则
n ! > Σ i = 1 n − 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)! 都比后面所有项的总和还大,则可以用类似进制转化的方法,不断除模得到得到 A A A 数组;
得到 A A A 数组后,即可得到 p i p_i pi 就是剩余未用的数中第 A i + 1 A_i + 1 Ai+1 小的;
3. 例子
以 1 , 2 , 3 , 4 , 5 {1, 2, 3, 4, 5} 1,2,3,4,5 的排列为例,求第 96 个排列;
首先 96 − 1 = 95 96 - 1 = 95 96−1=95 ;
95 / 4 ! = 3......23 95 / 4! = 3 ...... 23 95/4!=3......23 ,可知比它小的数有 3 个,即为 4 ;
23 / 3 ! = 3......5 23 / 3! = 3 ...... 5 23/3!=3......5 ,可知比它小的数有 2 个,因为 4 已经出现过,所以为 5 ;
5 / 2 ! = 2......1 5 / 2! = 2 ...... 1 5/2!=2......1 ,可知比它小的数有 2 个,即为 3 ;
1 / 1 ! = 1 1 / 1! = 1 1/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;
}