### [康托展开](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$大。 举个例子就明白这个公式了: 注意:计算的时候 $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$。 #### 康托展开代码 ```cpp {.line-numbers} #include 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 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 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; } ```