5.9 KiB
康托展开
官方简介:
康托展开 是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
通俗简介:
-
康托展开 可以求解一个排列的序号,比如:
12345
序号为1
,12354
序号为2
,按字典序增加编号递增,依次类推 -
康托逆展开 可以求解一个序号它对应的排列是什么
康托展开解释: 先给出康托展开的公式:
\large X=a_n \cdot (n-1)!+a_{n-1}\cdot(n-2)!+...+a_1\cdot 0
先对这个公式里变量进行解释,大家不理解这个公式没关系,慢慢往后看,很简单的。
a_i
的意思是从右往左数第 i
位这个数是这个数前未出现的数,第a_i
大。
举个例子就明白这个公式了:
注意:计算的时候 12345
序列应视为第0
个序列,后面会解释为什么。
拿52413
举例子:
- 首先看第一个数
5
,不管第一位是什么数,后面都有四位数,那么这四位数全排列的方式有4!
种,而如果第一位是1
或2
或3
或4
都会比5
开头的字典序要小,所以可以令1,2,3,4
分别作为开头,这样的话就会有4 * 4!
种排法要比52413
这种排法的字典序要小。
那么第一个数是1,2,3,4
时候的字典序的个数数完了是 4 * 4!
种,且这些字典序都要比52413
的字典序要小。
还有其他的排列方式比52413
的字典序要小的吗?
-
那么就可以固定第一位
5
,找下一位2
,这时5
已经用过了,所以从剩下的1,2,3,4
里挑选比2
小的数,一共1
个,后面还剩三位,也就是3!
种排列方式,那么这时候比52413
字典序要小的又有1 * 3!
种,也就是当5
在第一位,1
在第二位的时候。 -
再看第三位
4
,这时5,2
都用了,所以从剩下的1,3,4
三个数中找比4
小的数的个数,有两个比4
小原理同上,所以这时候也可以有2 * 2!
种排列方式的字典序小于52413
-
再看第四位
1
,这时候会有0 * 1!
种 -
再看第五位
3
,这时候会有0 * 0!
种
综上所述:
对于序列: 52413
该序列展开后为: 4 * 4! + 1 * 3! + 2 * 2! + 0 * 1! + 0 * 0!
,计算结果是: 106
由于是从0
开始计数的,所以最后 52413
的编号为 107
Q
:为什么从0
开始计数?
可以这样看:我现在让你求12345
的康托展开值,也就是:
0*4!+ 0*3!+ 0*2!+ 0*1!+0*0! = 0
所以明白了吧~~
康托公式最小字典序的编号就是0
。
康托展开代码
#include <bits/stdc++.h>
using namespace std;
// 阶乘表
int fact[20];
void init_fact(int n) {
fact[0] = 1; // 0的阶乘为1
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
}
// 康托展开
int cantor(string str) {
int x = 1; // 注意,因为 12345 是算作0开始计算的,最后结果要把12345看作是第一个
int len = str.size();
for (int i = 0; i < len; i++) {
int smaller = 0; // 在当前位之后小于其的个数
for (int j = i + 1; j < len; j++)
if (str[i] > str[j]) smaller++;
// 两种方法:
// ① 往后看,计算str[i]是第几大的数,或者说计算有几个比他小的数
// ② 往前看,计算出现了几个比自己小的数,从自己身上扣除掉逆序数量,也就是后面剩余比自己小的数量
x += smaller * fact[len - i - 1]; // 乘上阶乘
}
return x;
}
int main() {
// 初始化10以内的阶乘表
init_fact(10);
string str = "52413";
// 康托展开
cout << cantor(str) << endl;
return 0;
}
逆康托展开
这里直接开栗子:
如果初始序列是12345
(第一个),让你求第107
个序列是什么。(按字典序递增)
这样计算:
先把107
减1
,因为康托展开里的初始序列编号为0
然后计算下后缀积:
1 |
2 |
3 |
4 |
5 |
|
---|---|---|---|---|---|
5! |
4! |
3! |
2! |
1! |
0! |
120 |
24 |
6 |
2 |
1 |
1 |
106 / 4! = 4 ······ 10
有4
个比它小的所以因该是5
从(1,2,3,4,5)
里选10 / 3! = 1 ······ 4
有1
个比它小的所以因该是2
从(1, 2, 3, 4)
里选4 / 2! = 2 ······ 0
有2
个比它小的所以因该是4
从(1, 3, 4)
里选0 / 1! = 0 ······ 0
有0
个比它小的所以因该是1
从(1,3)
里选0 / 0! = 0 ······ 0
有0
个比它小的所以因该是3
从(3)
里选
所以编号107
的是 52413
#include <bits/stdc++.h>
using namespace std;
int fact[20];
void init_fact(int n) {
fact[0] = 1; // 0的阶乘为1
for (int i = 1; i <= n; i++) fact[i] = fact[i - 1] * i;
}
string ans;
// 逆康托展开
void getPermutation(int n, int k) {
vector<char> v; // 存放当前可选数,有序
for (int i = 1; i <= n; i++) v.push_back(i + '0');
k--; // 要先 -1 才是其康托展开的值
for (int i = n; i; i--) {
int r = k % fact[i - 1];
int t = k / fact[i - 1];
k = r;
ans += v[t]; // 剩余数里第t+1个数为当前位
v.erase(v.begin() + t); // 移除选做当前位的数
}
}
int main() {
int n = 5, k = 107; // 107
init_fact(10); // 预处理阶乘
getPermutation(n, k);
cout << ans << endl;
return 0;
}