康托展开

康托展开

康托展开可以用来求一个 \(1\sim n\) 的任意排列的排名。

康托展开可以在 \(O(n^2)\) 的复杂度内求出一个排列的排名,在用树状数组优化时可以做到 \(O(n\log n)\)

前置知识

什么是排列(前置知识 1)

\(n\) 的排列是 \(1\)\(n\) 各出现一次的序列,总共有 \(n!\) 个不同排列,例如 \(\left[2,4,1,3\right]\) 是长度为 \(4\) 的排列之一。

排列的比较(前置知识 2)

按照从前往后的顺序逐个数字经行比较,第一个不同的数决定排列大小(通常只比较长度相同的排列),例如长度为 \(4\) 的排列 \(\left[2,3,4,1\right]\) 小于 \(\left[2,4,1,3\right]\)

排列的编号(前置知识 3)

由于排列有大小之分,所以可以把排列从小到大排列在一起,第 \(k\) 小的给定编号 \(k\)

康托展开

实现

位数 位权
\(1\) \(0\)
\(2\) \(1\)
\(3\) \(2\)
\(4\) \(6\)
... ...
\(n\) \((n+1)!\)

我们知道长为 \(5\) 的排列 \(\left[2,5,3,4,1\right]\) 大于以 \(1\) 为第一位的任何排列,以 \(1\) 为第一位的 \(5\) 的排列有 \(4!\) 种。这是非常好理解的。但是我们对第二位的 \(5\) 而言,它大于第一位与这个排列相同的,而这一位比 \(5\) 小的所有排列。不过我们要注意的是,这一位不仅要比 \(5\) 小,还要满足没有在当前排列的前面出现过,不然统计就重复了。因此这一位为 \(1,3\)\(4\),第一位为 \(2\) 的所有排列都比它要小,数量为 \(3\times 3!\)

按照这样统计下去,答案就是 \(1+4!+3\times 3!+2!+1=46\)。注意我们统计的是排名,因此最前面要 \(+1\)

注意到我们每次要用到 当前有多少个小于它的数还没有出现,这里用树状数组统计比它小的数出现过的次数就可以了。

例题 【模板】康托展开

题意

\(1\sim n\) 的一个给定全排列在所有 \(1\sim n\) 全排列中的排名。结果对 \(998244353\) 取模。

代码

#include <bits/stdc++.h>
#define int long long

using namespace std;

const int kMaxN = 1e6 + 10, kMod = 998244353;

int d[kMaxN], f[kMaxN], n, x, ans;

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

void kModify(int x, int o) {
  for (; x <= n; x += lowbit(x)) {
    d[x] += o;
  }
}

int query(int x) {                                    // 求 x 是剩下的第几个
  int ret = 0;
  for (; x >= 1; x -= lowbit(x)) {
    ret += d[x];
  }
  return ret;
}

signed main() {
  cin >> n;
  f[0] = 1;
  for (int i = 1; i <= n; i++) {
    d[i] = lowbit(i);                                 // O(n) 建树
    f[i] = (f[i - 1] * i) % kMod;                     // 预处理阶乘
  }
  for (int i = 1; i <= n; i++) {
    cin >> x;                                         // 读入第 i 个数字
    kModify(x, -1);
    ans = (ans + query(x) * f[n - i] % kMod) % kMod;  // 计算答案
  }
  cout << ans + 1 << '\n';
  return 0;
}

逆康托展开

因为排列的排名和排列是一一对应的,所以康托展开满足双射关系,是可逆的。可以通过类似上面的过程倒推回来。

如果我们知道一个排列的排名,就可以推出这个排列。因为 \(4!\) 是严格大于 \(3\times 3!+2\times 2!+1\times 1!\) 的,所以可以认为对于长度为 \(5\) 的排列,排名 \(x\) 除以 \(4!\) 向下取整就是有多少个数小于这个排列的第一位。

示例

我们用长度为 \(5\) 的序列编号为 \(46\) 的为示例。首先让 \(46-1=45\)\(45\) 代表着有多少个排列比这个排列小。

\(\lfloor\frac {45}{4!}\rfloor=1\),有一个数小于它,所以第一位是 \(2\)

此时让排名减去 \(1\times 4!\) 得到 \(21\)\(\lfloor\frac {21}{3!}\rfloor=3\),有 \(3\) 个数小于它,去掉已经存在的 \(2\),这一位是 \(5\)

\(21-3\times 3!=3\)\(\lfloor\frac {3}{2!}\rfloor=1\),有一个数小于它,那么这一位就是 \(3\)。让 \(3-1\times 2!=1\),有一个数小于它,这一位是剩下来的第二位,\(4\),剩下一位就是 \(1\)。即 \([2,5,3,4,1]\)

实际上我们得到了形如有两个数小于它这一结论,就知道它是当前第 \(3\) 个没有被选上的数,这里也可以用线段树维护,时间复杂度为 \(O(n\log n)\)

posted @ 2024-07-22 10:12  小熊涛涛  阅读(6)  评论(0编辑  收藏  举报