前置知識:樹狀數組
前導
康託展開(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]; // 重名了用 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;
}
逆展開樹狀數組倍增
做法,總時間複雜度
:
#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;
}