康托展开与逆康托展开
康托展开
康托展开是用于求一个 排列 在 全排列 中的 位次 的算法。
以 a=52413a=52413a=52413 为例:
- a1a_1a1 是 555,对于一个排列 bbb 而言,当 b1∈{1,2,3,4}b_1\in\{1,2,3,4\}b1∈{1,2,3,4} 的时候有 b<ab<ab<a,这样的 bbb 有 4×4!4\times 4!4×4! 个。
- a2a_2a2 是 222,对于一个排列 bbb 而言,当 b1=a1b_1=a_1b1=a1,b2=1b_2=1b2=1 时有 b<ab<ab<a,这样的 bbb 有 1×3!1\times 3!1×3! 个。
- a3a_3a3 是 444,对于一个排列 bbb 而言,前缀与 aaa 相同,当 b3∈{1,3}b_3\in\{1,3\}b3∈{1,3} 时,有 b<ab<ab<a,这样的 bbb 有 2×2!2\times 2!2×2! 个。
- a4a_4a4 是 111,对于一个排列 bbb 而言,前缀与 aaa 相同,不存在 b<ab<ab<a。
- a5a_5a5 是 333,对于一个排列 bbb 而言,前缀与 aaa 相同,不存在 b<ab<ab<a。
所以 aaa 的位次相当于求有多少个 bbb 小于 aaa,可知有 106106106 个排列小于 aaa,所以 aaa 的位次是 107107107。
总结一下求位次的过程就是:
- 对于当前位置 aia_iai,求 出剩下可用的数有多少个小于 aia_iai,设为 cntcntcnt。
- 对于当前位置 aia_iai,求出 后面还有几个数没排,设为 bntbntbnt。
- 累加答案 cnt×bnt!cnt\times bnt!cnt×bnt!。
- 从剩下的可用的数中删去 aia_iai,并前往下一个位置。
可以发现 bnt!bnt!bnt! 是可以预处理的,现在的问题就是如何动态维护剩下的数,且能查询出有多少个数小于 aia_iai?
因为 ai≤na_i\le nai≤n,所以可以使用 权值树状数组 求解。
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int MOD = 998244353;
const int N = 1e6 + 7;
template<typename T>// 单点修改和区间查询的树状数组
struct Fenwick {vector <T> tree;int n;Fenwick(int _n) : n(_n), tree(_n + 2){}// 求 1~x 的前缀和T getsum(int x) {T ans = 0;int L = x;while (x) {ans += tree[x];x = x - lowbit(x);}return ans;}// 区间查询T get_range_sum(int l, int r) {return getsum(r) - getsum(l - 1);}// 单点修改void add(int x, T k) {while (x <= n) {tree[x] += k;x += lowbit(x);}}static inline int lowbit(int x) {return x & (-x);}T getKth(int k) {T x = 0, sum = 0;for (int i = log2(n); i >= 0; i--) {x += (1ll << i);if (x >= n || sum + tree[x] >= k) {x -= (1ll << i);} else {sum += tree[x];}}return x + 1;}
};int fac[N];
void init(int n) {fac[0] = 1;for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
}int Cantor(int n, vector <int> &p) {Fenwick<int> tree(n);for (int i = 1; i <= n; i++) {tree.add(i, 1);}int ans = 0;for (int i = 1; i <= n; i++) {int cnt = tree.get_range_sum(1, p[i] - 1);int bnt = fac[n - i];ans = (ans + cnt * bnt % MOD) % MOD;tree.add(p[i], -1);}return ans;
}
void slove () {int n;cin >> n;vector <int> p(n + 1);for (int i = 1; i <= n; i++) cin >> p[i];init(n);int ans = Cantor(n, p);cout << ans + 1 << endl;
}signed main () {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);slove();
}
逆康托展开
当我们知道了一个排列的位次,如何求出具体排列?
根据康托展开,我们可以知道一个排列的位次公式:
a1×(n−1)!+a2×(n−2)!+⋯+an×0!
a_1\times(n-1)!+a_2\times(n-2)!+\cdots +a_n\times 0!
a1×(n−1)!+a2×(n−2)!+⋯+an×0!
下面给出求出具体排列的步骤:
设当前位次是 pospospos,当前要确定的是排列的第 iii 项,
- 判断 pospospos 是否大于等于 (n−i)!(n-i)!(n−i)!。
- 求出系数 k=pos/(n−i)!k=pos/(n-i)!k=pos/(n−i)!,说明 当前剩下的数里有 kkk 个小于 a[i]a[i]a[i],那么 a[i]a[i]a[i] 就是剩下的数里的第 k+1k+1k+1 小的数。
- 将 pospospos 对 (n−i)!(n-i)!(n−i)! 取模。
- 接下来确定第 i+1i+1i+1 项的值。
因为我们需要进行除法、取模的操作,所以不能再模 ModModMod 的意义下出现,这也意味着 n!≤264−1n!\le 2^{64}-1n!≤264−1。
否则就会爆 longlonglong longlonglong。
可以发现阶乘是可以预处理的,然后就是维护剩下的数,并 查询第 k+1k+1k+1 小的数,这个可以用 权值树状数组 来求解。
下述代码是包含康托展开和逆康托展开的代码,值得注意的是,如果给定某一排列的位次,进行逆康托展开的时候,记得将位次减 111,因为我们需要的是小于该排列的数量。
同理,进行康托展开的时候,把答案加 111,因为求出来的是小于当前排列的数量。
切记不要取模。
#include <bits/stdc++.h>
using namespace std;
//#pragma GCC optimize(2)
#define int long long
#define endl '\n'
#define PII pair<int,int>
#define INF 1e18
const int MOD = 998244353;
const int N = 1e6 + 7;
template<typename T>// 单点修改和区间查询的树状数组
struct Fenwick {vector <T> tree;int n;Fenwick(int _n) : n(_n), tree(_n + 2){}// 求 1~x 的前缀和T getsum(int x) {T ans = 0;int L = x;while (x) {ans += tree[x];x = x - lowbit(x);}return ans;}// 区间查询T get_range_sum(int l, int r) {return getsum(r) - getsum(l - 1);}// 单点修改void add(int x, T k) {while (x <= n) {tree[x] += k;x += lowbit(x);}}static inline int lowbit(int x) {return x & (-x);}// 求区间第 k 小T getKth(int k) {T x = 0, sum = 0;for (int i = log2(n); i >= 0; i--) {x += (1ll << i);if (x >= n || sum + tree[x] >= k) {x -= (1ll << i);} else {sum += tree[x];}}return x + 1;}
};int fac[N];
void init(int n) {fac[0] = 1;for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i;
}// 下标从 1 开始
int Cantor(int n, vector <int> &p) {Fenwick<int> tree(n);for (int i = 1; i <= n; i++) {tree.add(i, 1);}int ans = 0;for (int i = 1; i <= n; i++) {int cnt = tree.get_range_sum(1, p[i] - 1);int bnt = fac[n - i];ans = (ans + cnt * bnt);tree.add(p[i], -1);}return ans;
}vector <int> AntiCantor(int n, int pos) {vector <int> p (n + 1);Fenwick<int> tree(n);for (int i = 1; i <= n; i++) {tree.add(i, 1);}for (int i = 1; i <= n; i++) {int k = pos/fac[n - i];pos %= fac[n - i];p[i] = tree.getKth(k + 1);tree.add(p[i], -1);}return p;
}void slove () {int n, q;cin >> n >> q;init(n);while (q--) {char op;cin >> op;if (op == 'P') {int pos;cin >> pos;vector <int> p = AntiCantor(n, pos - 1);for (int i = 1; i <= n; i++) cout << p[i] << ' ';cout << endl;} else {vector <int> p(n + 1);for (int i = 1; i <= n; i++) cin >> p[i];cout << Cantor(n, p) + 1 << endl;}}}signed main () {ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);slove();
}