康托展开&逆康托展开
1.算法分析
康托展开可以求一个序列是第几个排列,即求得[2, 1, 3]是第3个排列
逆康托展开可以求得第k个排列是多少,即求得第3个排列为[2, 1, 3]
基于这个性质,可以使用康托展开把一个序列做哈希,映射为一个数字。
1.1 康托展开
康托展开公式:当前排列的的排名为: r a n k = a n ∗ ( n − 1 ) ! + a n − 1 ∗ ( n − 2 ) ! + . . . + a 1 ∗ 0 + 1 rank = a_n*(n - 1)! + a_{n - 1}*(n - 2)! + ... + a_1 * 0 + 1 rank=an∗(n−1)!+an−1∗(n−2)!+...+a1∗0+1
其中,其中a[i]表示第i个数在未出现的数中排第几。
朴素的想法为当遍历到当前数字a[i]时,向前扫描,看有多少个还没有用过的数字。这样为 O ( n 2 ) O(n^2) O(n2)
但是可以记使用过为0,没有使用过为1,这样可以使用树状数组维护前缀和,即可知道当前数字在没有用过的数字中排第几。
1.2 逆康托展开
逆康托展开是康托展开的逆过程,倒着从(n-1)!的阶乘开始往1!做除法,然后按照康托展开的逻辑求即可。
以[4,2,5,1,3]为例:
- 82/(4!),商为3,余数为10:那么要找没有出现过的第3+1=4个数字,即4.
- 10/(3!),商为1,余数为4:那么要找没有出现过的第1+1=2个数字,即2.
- 4/(2!),商为2,余数为0:那么要找没有出现过的第2+1=3个数字,即5.
- 0/(1!),商为0,余数为0:那么要找没有出现过的第0+1=1个数字,即1.
- 最后将未填充的数3填到最后一位
基于上述思想可以发现核心问题就找没有出现过的第k个数字,朴素思想就是用二分+树状数组实现。但是利用
2.模板
#include <bits/stdc++.h>
using namespace std;
int const N = 2e6 + 10, MOD = 998244353;
typedef long long LL;
LL fact[N];
int c[N], n, m, a[N]; // n为序列长度,m为大于n的第一个2的幂次方,注意这里c数组空间要开双倍
int lowbit(int x) {
return x & (-x);
}
// 单点修改
void add(int x, int y) {
for (int i = x; i <= m; i += lowbit(i)) c[i] += y; // 不断往父节点跳
}
// 找到x属于还没有用过中的第几个
int rnk<