You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

5.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

康托展开

官方简介:

康托展开 是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。

通俗简介

  • 康托展开 可以求解一个排列的序号,比如: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 种,而如果第一位是 1234 都会比5开头的字典序要小,所以可以令1234分别作为开头,这样的话就会有 4 * 4种排法要比 52413 这种排法的字典序要小。

那么第一个数是1234时候的字典序的个数数完了是 4 * 4 种,且这些字典序都要比52413的字典序要小。

还有其他的排列方式比52413的字典序要小的吗?

  • 那么就可以固定第一位5,找下一位2,这时5已经用过了,所以从剩下的 1234 里挑选比2小的数,一共1个,后面还剩三位,也就是3种排列方式,那么这时候比 52413 字典序要小的又有 1 * 3种,也就是当5在第一位,1在第二位的时候。

  • 再看第三位4,这时52都用了,所以从剩下的 134三个数中找比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个序列是什么。(按字典序递增)

这样计算:

先把1071,因为康托展开里的初始序列编号为0

然后计算下后缀积:

1 2 3 4 5
5 4! 3! 2! 1! 0!
120 24 6 2 1 1
  • 106 / 4! = 4 ······ 104个比它小的所以因该是512345里选
  • 10 / 3! = 1 ······ 41个比它小的所以因该是21 2 3 4里选
  • 4 / 2! = 2 ······ 02个比它小的所以因该是41 3 4里选
  • 0 / 1! = 0 ······ 00个比它小的所以因该是113里选
  • 0 / 0! = 0 ······ 00个比它小的所以因该是33里选

所以编号107的是 52413

LetCoode 60. 排列序列

#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;
}