康托展开式与逆康托展开式

康托展开

官方简介:

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

通俗简介

  • 康托展开 可以求解一个排列的序号,比如:\(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\)

\(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;
}
posted @ 2023-03-16 14:11  糖豆爸爸  阅读(97)  评论(0编辑  收藏  举报
Live2D