【算法速成课 3】康托展开(Cantor Expansion)/ 题解 P3014 [USACO11FEB] Cow Line S
专栏:算法速成课_proMatheus的博客-CSDN博客
前置知识:树状数组
前导
康托展开(Cantor Expansion)是一种将一个排列,映射为一个唯一整数的编码方法。
常用于排列的哈希、状态压缩或字典序编号等场景。
题意
任务一:求一个全排列是第几个全排列,按字典序(即从小到大)。
任务二:求第 个全排列。
1.康托展开(任务 1)
分析
假如我问你,求 是第几个
的全排列,你会怎么做?
先一个个列出来?
发现是第 个。
那有没有更快的方法?
我们发现 的第二个位是
,这代表
都在它的前面。
直接加上所有 的数量
。
接着发现第三个位是 ,照理来说应该是它后面、比它小的
在它这个位置,
但现在这里是 ,代表
都在
的前面,加上数量
。
,这是所有在
前面的数,即
是第
个全排列。
再回顾总结下,假设要求 的全排列。
如果有比当前第 个位上的数
小,且还没出现过的数
。
那么这个 肯定能顶替
得到更小的字典序,排在题目给出排列的前面(
到
位不动)。
累计答案加上 ,这表示所有
顶替
构成的排列都排在题目给出排列的前面。
很明显,有几个这样的 就应该加几个
。
实现
例题:P5367 【模板】康托展开 - 洛谷
想要求出比当前 小且还没出现过数的个数,可以考虑使用树状数组 / 线段树 / 平衡树。
后两个都稍有麻烦,我们用树状数组。
时间复杂度 。
代码:
#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):
#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]; // 重名了用 aaauto 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;
}
逆展开树状数组倍增 做法,总时间复杂度
:
#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]; // 重名了用 aaint t = kth(aa + 1); // 第 (aa + 1) 小,kth 是 1-indexeda[i] = t;add(t, -1); // 删掉这个数k -= aa * fac[n - i]; // 别忘了减 }for (int i = 1; i <= n; i ++) {cout << a[i] << " ";}cout << "\n";}}return 0;
}
