康托展开

简介

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

简单来说,通过康托展开可以确定一个给定的 \(n\) 位数的排列是字典序中的第几个排列(排名)

康托展开

给定一个 \(n\) 位数的排列 \(a_1,a_2\cdots a_n\) ,则康托展开值 \(k\) 有如下公式:

\[k=1+\sum_{i=1}^n A_i\times(n-i)! \]

其中 \(A_i=\sum_{j=i+1}^n [a_j<a_i]\) ,表示在原排列中排在 \(a_i\) 后且比 \(a_i\) 小的数的个数

\(4,1,5,3,2\) 为例:

  • 排在 \(4\) 后且小于 \(4\) 的有 \(3\) 个,所以 \(x=1+3\times4!\)
  • 排在 \(1\) 后且小于 \(1\) 的有 \(0\) 个,所以 \(x=1+72+0\)
  • 排在 \(5\) 后且小于 \(5\) 的有 \(2\) 个,所以 \(x=1+72+0+2\times2!\)
  • 排在 \(3\) 后且小于 \(3\) 的有 \(1\) 个,所以 \(x=1+72+0+4+1\times1!\)

以此类推,可以算出此排列的排名是 \(78\)

这样做的原因是:排在 \(a_i\) 后且比 \(a_i\) 小的数都可以放到 \(a_i\) 的位置,并且这样以后第 \(i+1\sim n\) 位上的数任意排列,得到的新排列排名都小于原排列。根据乘法原理,新排列的个数为 \(A_i\times(n-i)!\)

暴力:
int ans = 1;
int fact[0] = 1;
for(int i = 1; i <= n; i++)
    fact[i] = fact[i - 1] * i;
for(int i = 1; i <= n; i++) {
    int cnt = 0;
    for(int j = i + 1; j <= n; j++)
        if(a[j] < a[i])
            cnt++;
    ans += cnt * fact[n - i];
}

时间复杂度 \(O(n^2)\)

优化:

可以发现,暴力做法的查找效率较低,那么如何优化呢?可以考虑用树状数组

我们建立一个数组 \(c\) ,当遍历到 \(a_i\) 时,就把 \(c[a[i]]\) 设为 \(true\) ,那么数组 \(c\) 的前缀和 \(sum[a[i]-1]\) 就表示在 \(a_i\) 前面且小于 \(a_i\) 的数的个数,那么在 \(a_i\) 后且比 \(a_i\) 小的数的个数即为 \(a[i]-1-sum[a[i]-1]\) ,这样就可以用树状数组维护了

Luogu 康托展开

#include<bits/stdc++.h>
using namespace std;

const int MOD = 998244353;
const int MAX_N = 1000000 + 5;
int n;
int a[MAX_N];
int c[MAX_N];
int fact[MAX_N];

int lowbit(int x)
{
    return x & (-x);
}

void update(int x, int k)
{
    while(x <= n) {
        c[x] += k;
        x += lowbit(x);
    }
}

int get_sum(int x)
{
    int res = 0;
    while(x >= 1) {
        res += c[x];
        x -= lowbit(x);
    }
    return res;
}

int main()
{
    scanf("%d", &n);
    fact[0] = 1;
    for(int i = 1; i <= n; i++)
        fact[i] = 1ll * fact[i - 1] * i % MOD;
    for(int i = 1; i <= n; i++)
        scanf("%d", &a[i]);
    int ans = 1;
    for(int i = 1; i <= n; i++) {
        update(a[i], 1);
        ans = (ans + 1ll * fact[n - i] * (a[i] - get_sum(a[i] - 1) - 1) % MOD) % MOD;
    }
    printf("%d\n", ans);
    return 0;
}

时间复杂度 \(O(n\log n)\)

逆康托展开

康托展开的逆运算,求的是在 \(1\sim n\) 的全排列中排名第 \(k\) 的排列

\(n=5,k=78\) 为例:

  • \(k-1=77\)

  • \(77\div 4!=3\cdots5\) ,比第一个数小且没出现过的数有 \(3\) 个, \(a[1]=4\)

  • \(5\div 3!=0\cdots5\) ,比第二个数小且没出现过的数有 \(0\) 个, \(a[2]=1\)

  • \(5\div 2!=2\cdots1\) ,比第三个数小且没出现过的数有 \(2\) 个, \(a[3]=5\)

  • \(1\div 1!=1\cdots 0\) ,比第四个数小且没出现过的数有 \(1\) 个, \(a[4]=3\)

所以所求排列为 \(4,1,5,3,2\)

#include<bits/stdc++.h>
using namespace std;

int n, k;
int res[15], fact[15];
bool vis[15];

int main()
{
    scanf("%d%d", &n, &k);
    fact[0] = 1;
    for(int i = 1; i <= n; i++)
        fact[i] = fact[i - 1] * i;
    k--;
    int t, r = k;
    for(int i = 1; i <= n; i++) {
        t = r / (fact[n - i]);
        r = r % fact[n - i];
        for(int j = 1; j <= n; j++) {
            if(!vis[j]) {
                if(t == 0) {
                    vis[j] = true;
                    res[i] = j;
                    break;
                }
                t--;
            }
        }
    }
    for(int i = 1; i <= n; i++)
        printf("%d\n", res[i]);
    return 0;
}
posted @ 2021-12-18 17:49  f(k(t))  阅读(53)  评论(0编辑  收藏  举报