Files
python/TangDou/AcWing/KangTuo/康托展开.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

156 lines
5.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### [康托展开](https://www.cnblogs.com/littlehb/p/17222367.html)
官方简介:
**康托展开** 是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
**通俗简介**:
- **康托展开** 可以求解一个排列的序号,比如:$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$大。
举个例子就明白这个公式了:
<font color='red' size=4><b>注意:计算的时候 $12345$ 序列应视为第$0$个序列,后面会解释为什么。</b></font>
拿$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$。
#### 康托展开代码
```cpp {.line-numbers}
#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$
[$LetCoode$ $60$. 排列序列](https://leetcode.cn/problems/permutation-sequence/submissions/)
```cpp {.line-numbers}
#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;
}
```