康托展开,逆康托展开,原理分析,题目练习
文章目录
- 一、康托展开
- 1.1 排列的排名
- 1.2 Lehmer 码
- 1.3 康托展开
- 1.4 模板
- 1.5 题目练习
- 1.5.1 P5367 【模板】康托展开
- 二、逆康托展开
- 2.1 逆康托展开
- 2.2 模板
- 2.3 题目练习
- 2.3.1 U72177 火星人plus
一、康托展开
1.1 排列的排名
n 个元素构成的排列的排名为 所有排列按字典序升序下的排名,比如 “1234” 的排名就是 1。
1.2 Lehmer 码
Lehmer 码(Lehmer code)是一种 阶乘进制(factorial number system)。这种进制中,不同的数位对应的底数(radix)并不相同。比如,十进制数
46
3
10
463_{10}
46310 可以在阶乘进制中表示为:
46
3
10
=
34101
0
!
463_{10} = 341010_{!}
46310=341010!
它表示如下含义
463
=
3
×
5
!
+
4
×
4
!
+
1
×
3
!
+
0
×
2
!
+
1
×
1
!
+
0
×
0
!
463 = 3\times 5! + 4\times 4! + 1\times 3! + 0\times 2! + 1\times 1! + 0\times 0!
463=3×5!+4×4!+1×3!+0×2!+1×1!+0×0!
1.3 康托展开
康托展开是指一种将自然数展开为数列的方法,类似于试填法 / 数位dp,从而求出排列的排名。
例如 求 452631 的排名,逐位试填:
- 第一位如果小于 4,那么可以填 1、2、3,剩下的随便填,贡献是 3 * 5!
- 第一位填4,第二位填小于5,那么可以填1、2、3,剩下随便填,贡献是 3 * 4!
- ……
- 因而 该排列的排名就是 1 + 3 * 5! + 3 * 4! + 1 * 3! + 2 * 2! + 1 * 1! + 0 * 0! = 444
加1 是因为我们试填算的是字典序严格小于该排列的排列数。
而我们试填的过程其实就是求出了该排列的 Lehmer 码的值。
那么就可以给出我们康托展开求排列排名的算法:
- 初始化 阶乘 fac = 1,排列长度为n,0 索引
- 倒序遍历该排列, 用权值树状数组维护当前加入树状数组的数字(平衡树类似的数据结构也可以)
- 假设 当前 下标为 i,用树状数组查询比 p[i] 小的数字 个数 为 cnt,那么该位置的贡献为 fac * cnt
- 然后更新 fac = (n - i) * cnt
- 假设求得的贡献和 为res,那么排名就是 res + 1
1.4 模板
模板中带取模,因为如果 求得排列长度很小比如 10,就没必要上康托展开了,当很大的时候一般都会取模。
或者存Lehmer 码 来避免求高精度(这在后面的逆康托展开中有涉及)
template<typename T>
class Fenwick {
private:
int n;
std::vector<T> tr;
public:
Fenwick(int _n) : n(_n), tr(_n + 1)
{}
Fenwick(const std::vector<T> &_init) : Fenwick(_init.size()) {
init(_init);
}
void init(const std::vector<T> &_init) {
for (int i = 1; i <= n; ++ i) {
tr[i] += _init[i - 1];
int j = i + (i & -i);
if (j <= n)
tr[j] += tr[i];
}
}
void add(int x, T k) {
for (; x <= n; x += x & -x) tr[x] += k;
}
void add(int l, int r, T k) {
add(l, k);
if (r + 1 <= n)
add(r + 1, -k);
}
T query(int x) const {
T res = T{};
for (; x; x &= x - 1) {
res += tr[x];
}
return res;
}
T query(int l, int r) const {
if (l > r) return T{};
return query(r) - query(l - 1);
}
int select(int k) {
int x = 0;
T cur{};
for (int i = 1 << std::__lg(n); i; i /= 2) {
if (x + i <= n && cur + tr[x + i] < k) {
x += i;
cur = cur + tr[x];
}
}
return x + 1;
}
void clear() {
tr.assign(n + 1, T{});
}
};
constexpr int P = 998244353;
int rank(std::vector<int> p) {
int n = p.size();
Fenwick<int> fen(n);
int res = 0;
for (int i = n - 1, fac = 1; i >= 0; -- i) {
res += 1LL * fen.query(p[i] - 1) * fac % P;
if (res >= P) {
res -= P;
}
fen.add(p[i], 1);
fac = 1LL * (n - i) * fac % P;
}
return res + 1;
}
1.5 题目练习
1.5.1 P5367 【模板】康托展开
原题链接
P5367 【模板】康托展开
思路分析
板子题,直接切
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
template<typename T>
class Fenwick {
private:
int n;
std::vector<T> tr;
public:
Fenwick(int _n) : n(_n), tr(_n + 1)
{}
Fenwick(const std::vector<T> &_init) : Fenwick(_init.size()) {
init(_init);
}
void init(const std::vector<T> &_init) {
for (int i = 1; i <= n; ++ i) {
tr[i] += _init[i - 1];
int j = i + (i & -i);
if (j <= n)
tr[j] += tr[i];
}
}
void add(int x, T k) {
for (; x <= n; x += x & -x) tr[x] += k;
}
void add(int l, int r, T k) {
add(l, k);
if (r + 1 <= n)
add(r + 1, -k);
}
T query(int x) const {
T res = T{};
for (; x; x &= x - 1) {
res += tr[x];
}
return res;
}
T query(int l, int r) const {
if (l > r) return T{};
return query(r) - query(l - 1);
}
int select(int k) {
int x = 0;
T cur{};
for (int i = 1 << std::__lg(n); i; i /= 2) {
if (x + i <= n && cur + tr[x + i] < k) {
x += i;
cur = cur + tr[x];
}
}
return x + 1;
}
void clear() {
tr.assign(n + 1, T{});
}
};
constexpr int P = 998244353;
int rank(std::vector<int> p) {
int n = p.size();
Fenwick<int> fen(n);
int res = 0;
for (int i = n - 1, fac = 1; i >= 0; -- i) {
res += 1LL * fen.query(p[i] - 1) * fac % P;
if (res >= P) {
res -= P;
}
fen.add(p[i], 1);
fac = 1LL * (n - i) * fac % P;
}
return res + 1;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> p(n);
for (int i = 0; i < n; ++ i) {
std::cin >> p[i];
}
std::cout << rank(p) << '\n';
return 0;
}
二、逆康托展开
2.1 逆康托展开
康托展开的逆问题,即给定排名求解相应的排列,只要将上述过程反过来操作即可。
- 实际上,在这一过程中求得的 Lehmer 码中的数字之和,就是排列的逆序数。
一个直接的方法是,预处理阶乘,然后权值树状数组初始化所有数字权值为1,然后顺序根据排名填写。
求lemher码一个比较好的避免预处理阶乘的方式是:
- 注意到 lehmer(i) <= n - i - 1 (0索引)
- 我们倒着填写(从n - 1 -> 0),假设求第k名排列
- lemher[i] = k % (n - i),k /= i
- 如此往左填写
为什么这么写是对的?
对于 lemher[n - 1],它的贡献是 lemher[n - 1] * 0!,然后我们除去了 1!,那么下一位即 lemher[n - 1] 的值就是 对 2取余
对于 lemher[n - 2],它的贡献是 lemher[n - 2] * 1!,然后我们除去了 2!,那么下一位即 lemher[n - 2] 的值就是 对 3取余
我们保证枚举到这一位的时候贡献已经通过k / (n - i) 等价为1,那么该位的值就是对n - i 取模
然后初始化树状数组每一位的权值为1,从左往右选取当前剩余的 lemher[i] + 1个数字
2.2 模板
std::vector<int> kth_permutation(int n, i64 k) {
-- k;
std::vector<int> lehmer(n);
for (int i = 1; i <= n; ++i) {
lehmer[n - i] = k % i;
k /= i;
}
Fenwick<int> fen((std::vector<int>(n, 1)));
std::vector<int> res(n);
for (int i = 0; i < n; ++i) {
res[i] = fen.select(lehmer[i] + 1);
fen.add(res[i], -1);
}
return res;
}
2.3 题目练习
2.3.1 U72177 火星人plus
原题链接
U72177 火星人plus
思路分析
比较麻烦的是涉及到高精度,怎么办?
求出 初始排列的lemher码,然后 末尾 += m,然后不断进位。然后根据lemher结合树状数组/平衡树填写即可。
AC代码
#include <bits/stdc++.h>
using i64 = long long;
using u64 = unsigned long long;
using u32 = unsigned;
using u128 = unsigned __int128;
template<typename T>
class Fenwick {
private:
int n;
std::vector<T> tr;
public:
Fenwick(int _n) : n(_n), tr(_n + 1)
{}
Fenwick(const std::vector<T> &_init) : Fenwick(_init.size()) {
init(_init);
}
void init(const std::vector<T> &_init) {
for (int i = 1; i <= n; ++ i) {
tr[i] += _init[i - 1];
int j = i + (i & -i);
if (j <= n)
tr[j] += tr[i];
}
}
void add(int x, T k) {
for (; x <= n; x += x & -x) tr[x] += k;
}
void add(int l, int r, T k) {
add(l, k);
if (r + 1 <= n)
add(r + 1, -k);
}
T query(int x) const {
T res = T{};
for (; x; x &= x - 1) {
res += tr[x];
}
return res;
}
T query(int l, int r) const {
if (l > r) return T{};
return query(r) - query(l - 1);
}
int select(int k) {
int x = 0;
T cur{};
for (int i = 1 << std::__lg(n); i; i /= 2) {
if (x + i <= n && cur + tr[x + i] < k) {
x += i;
cur = cur + tr[x];
}
}
return x + 1;
}
};
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n;
std::cin >> n;
i64 m;
std::cin >> m;
std::vector<int> p(n);
for (int i = 0; i < n; ++ i) {
std::cin >> p[i];
}
Fenwick<int> fen((std::vector<int>(n, 1)));
std::vector<i64> lehmer(n);
for (int i = 0; i < n; ++ i) {
lehmer[i] = fen.query(p[i] - 1);
fen.add(p[i], -1);
}
std::reverse(lehmer.begin(), lehmer.end());
lehmer[0] += m;
for (int i = 0; i + 1 < n; ++ i) {
lehmer[i + 1] += lehmer[i] / (i + 1);
lehmer[i] %= (i + 1);
}
while (lehmer.back() >= lehmer.size()) {
i64 x = lehmer.back() / lehmer.size();
lehmer.back() %= lehmer.size();
lehmer.push_back(x);
}
std::reverse(lehmer.begin(), lehmer.end());
fen = Fenwick<int>((std::vector<int>(lehmer.size(), 1)));
for (int i = 0; i < lehmer.size(); ++ i) {
int x = fen.select(lehmer[i] + 1);
std::cout << x << " \n"[i + 1 == lehmer.size()];
fen.add(x, -1);
}
return 0;
}