前置知识:树状数组
前导
康托展开(Cantor Expansion)是一种将一个排列 ,映射为一个唯一整数的编码方法。
常用于排列的哈希、状态压缩或字典序编号等场景。
题意
任务一:求一个全排列是第几个全排列,按字典序(即从小到大)。
任务二:求第 个全排列。
1.康托展开(任务 1)
分析
假如我问你,求 是第几个
的全排列,你会怎么做?
先一个个列出来?
发现是第 个。
那有没有更快的方法?
我们发现 的第二个位是
,这代表
都在它的前面。
直接加上所有 的数量
。
接着发现第三个位是 ,照理来说应该是它后面、比它小的
在它这个位置,
但现在这里是 ,代表
都在
的前面,加上数量
。
,这是所有在
前面的数,即
是第
个全排列。
再回顾总结下,假设要求 的全排列。
如果有比当前第 个位上的数
小,且还没出现过的数
。
那么这个 肯定能顶替
得到更小的字典序 ,排在题目给出排列的前面(
到
位不动)。
累计答案加上 ,这表示所有
顶替
构成的排列都排在题目给出排列的前面。
很明显,有几个这样的 就应该加几个
。
实现
想要求出比当前 小且还没出现过数的个数,可以考虑使用树状数组 / 线段树 / 平衡树。
后两个都稍有麻烦,我们用树状数组。
时间复杂度 。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10;
const LL P = 998244353;
int n, a[N];
LL c[N], fac[N];
void add(int x, LL d) {
for (; x <= n; x += x & -x) {
c[x] = (c[x] + d) % P;
}
}
LL get_sum(int x) {
LL res = 0;
for (; x >= 1; x -= x & -x) {
res = (res + c[x]) % P;
}
return res;
}
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
}
memset(c, 0, sizeof(c));
fac[0] = 1;
for (int i = 1; i <= n; i ++) { // 初始化阶乘数组
fac[i] = fac[i - 1] * i % P; // 一定要取模!!
}
LL ans = 0;
for (int i = 1; i <= n; i ++) {
int x = a[i];
LL t = ( (x - 1) - get_sum(x - 1) + P) % P;
// get_sum 求出来的是出现过比 x 小的数,要求没出现过的
ans = (ans + t * fac[n - i] % P) % P;
add(x, 1); // 把 x 放进去
}
cout << (ans + 1) << "\n"; // 别忘了加 1,ans 是在题目给出排列前面的排列数
return 0;
}
2.逆康托展开(任务 2)
分析
聪明的你肯定想到了应该怎么做:
假设给出的排列序号是 ,循环
到
,
先减减。
这样 就代表着在答案排列前面的排列个数 ,接下来每一个位都根据前面的排列定值。
当前循环到 。
如果 里面有
个
,取
到
位未出现 的第
小数为
。
那么当前位就等于 。
因为按理说当前位就该是 到
位未出现 的最小数了,但
里又有
个
。
这代表在第 位,还有**
个比它小的数构成的排列** 排在它前面(
到
位不动),
又由于不能重复,所以取 到
位未出现 的第
小数。
别忘了 。
还是拿 的例子,现在我们只知道
。
当 时,
里面有
个
,
到
位未出现 的第
小数为
。
当 时,
里面有
个
,
到
位未出现 的第
小数为
。
。
当 时,
里面有
个
,
到
位未出现 的第
小数为
。
。
当 时,
里面有
个
,
到
位未出现 的第
小数为
。
当 时,
里面有
个
,
到
位未出现 的第
小数为
。
得出 。
实现
难点在求 到
位未出现 的最小数,这玩意
,想优化上线段树 or 树状数组上倍增。
(不过例题 很小不需要,无所谓我会给出两份代码)
例题:P3014 [USACO11FEB] Cow Line S - 洛谷
P 操作就是逆展开。
,long long 的范围是
到
。
即差不多 ,可以放心用。
逆展开 代码(我用了 set,总时间复杂度
可 AC):
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 22;
int n, a[N];
LL c[N], fac[N];
void add(int x, LL d) {
for (; x <= n; x += x & -x) {
c[x] += d;
}
}
LL get_sum(int x) {
LL res = 0;
for (; x >= 1; x -= x & -x) {
res += c[x];
}
return res;
}
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
int K;
cin >> n >> K;
fac[0] = 1;
for (int i = 1; i <= n; i ++) {
fac[i] = fac[i - 1] * i; // 阶乘这一块 /.
}
while (K --) {
char s[5];
cin >> s;
if (s[0] == 'Q') { // 正展开
memset(c, 0, sizeof(c)); // 每个询问都要清空一次
LL ans = 0;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
LL t = a[i] - 1 - get_sum(a[i] - 1);
ans += t * fac[n - i];
add(a[i], 1);
}
cout << (ans + 1) << "\n";
}
else { // 逆展开
LL k; // 这玩意可有 20! 那么大
cin >> k; k --; // 减减
set<int> set_; // 未使用数字集合
for (int i = 1; i <= n; i ++) {
set_.insert(i); // 全都放进去
}
for (int i = 1; i <= n; i ++) {
int aa = k / fac[n - i]; // 重名了用 aa
auto it = set_.begin();
advance(it, aa); // 移到第 aa + 1 个元素
a[i] = *it;
set_.erase(*it); // 删了
k -= aa * fac[n - i]; // 别忘了减
}
for (int i = 1; i <= n; i ++) {
cout << a[i] << " ";
}
cout << "\n";
}
}
return 0;
}
逆展开树状数组倍增 做法,总时间复杂度
:
cpp
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 22;
int n, a[N];
LL c[N], fac[N];
void add(int x, LL d) {
for (; x <= n; x += x & -x) {
c[x] += d;
}
}
LL get_sum(int x) {
LL res = 0;
for (; x >= 1; x -= x & -x) {
res += c[x];
}
return res;
}
// 在树状数组 c 中找第 k 小的可用数(k 从 1 开始)
// 这里利用了树状数组的特性,即查询 1 到 n 最多遍历 log n 个值
int kth(int k) {
int p = 0, s = 0;
// p 是当前树状数组上刚刚遍历到的节点(当前遍历范围可使用数字最大值)
// s 是当前已遍历范围里可使用数字的总个数
// 整个过程就是不断调整 p 的大小(遍历范围的最大值)
// 来看看 s 什么时候刚好等于 k - 1
// 由于最后 s 肯定停在 k - 1 的临界值(再大一点就不是了)
// 所以 p + 1 是第 k 个数
// n <= 20,所以 1 << 5 = 32 足够
for (int i = 5; i >= 0; i --) {
int t = p + (1 << i);
if (t <= n && s + c[t] < k) { // 节点还小于 n,总个数要小于 k
s += c[t];
p = t;
}
}
// 现在 p 是最大的满足 get_sum(p) < k 的下标
return p + 1;
}
int main () {
ios::sync_with_stdio(false);
cin.tie(0);
int K;
cin >> n >> K;
fac[0] = 1;
for (int i = 1; i <= n; i ++) {
fac[i] = fac[i - 1] * i; // 阶乘这一块 /.
}
while (K --) {
char s[5];
cin >> s;
if (s[0] == 'Q') { // 正展开
memset(c, 0, sizeof(c)); // 每个询问都要清空一次
LL ans = 0;
for (int i = 1; i <= n; i ++) {
cin >> a[i];
LL t = a[i] - 1 - get_sum(a[i] - 1);
ans += t * fac[n - i];
add(a[i], 1);
}
cout << (ans + 1) << "\n";
}
else { // 逆展开
LL k; // 这玩意可有 20! 那么大
cin >> k; k --; // 减减
memset(c, 0, sizeof(c)); // 现在这个树状数组是存未使用的数字
for (int i = 1; i <= n; i++) {
add(i, 1); // 全都放进去
}
for (int i = 1; i <= n; i ++) {
int aa = k / fac[n - i]; // 重名了用 aa
int t = kth(aa + 1); // 第 (aa + 1) 小,kth 是 1-indexed
a[i] = t;
add(t, -1); // 删掉这个数
k -= aa * fac[n - i]; // 别忘了减
}
for (int i = 1; i <= n; i ++) {
cout << a[i] << " ";
}
cout << "\n";
}
}
return 0;
}