前置知識:樹狀數組


前導

康託展開(Cantor Expansion)是一種將一個排列,映射為一個唯一整數的編碼方法。

常用於排列的哈希、狀態壓縮或字典序編號等場景。

題意

任務一:求一個全排列是第幾個全排列,按字典序(即從小到大)。

任務二:求第 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

個全排列。

1.康託展開(任務 1)

分析

假如我問你,求 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 是第幾個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_03

 的全排列,你會怎麼做?

先一個個列出來?

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_04

發現是第 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_05

 個。

那有沒有更快的方法?

我們發現 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 的第二個位是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_07

,這代表 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_08

 都在它的前面。直接加上所有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_08

 的數量 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_10

。接着發現第三個位是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_11

,照理來説應該是它後面、比它小的 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12

 在它這個位置,但現在這裏是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_11

,代表 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_14

 都在 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 的前面,加上數量 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12


luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_17

,這是所有在 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 前面的數,即 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 是第 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_05

 個全排列。再回顧總結下,假設要求 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_21

 的全排列。如果有比當前第 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_22

 個位上的數 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_23

 小,且還沒出現過的數 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

。那麼這個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 肯定能頂替 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_23

 得到更小的字典序,排在題目給出排列的前面(

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位不動)。累計答案加上 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_29

,這表示所有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 頂替 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_23

 構成的排列都排在題目給出排列的前面。很明顯,有幾個這樣的 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 就應該加幾個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_29


實現

例題:P5367 【模板】康託展開 - 洛谷

想要求出比當前 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_23

 小且還沒出現過數的個數,可以考慮使用樹狀數組 / 線段樹 / 平衡樹。

後兩個都稍有麻煩,我們用樹狀數組

時間複雜度 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_35


代碼:

#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)

分析

聰明的你肯定想到了應該怎麼做:

假設給出的排列序號是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

,循環 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_21


luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 先減減。這樣 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 就代表着在答案排列前面的排列個數,接下來每一個位都根據前面的排列定值。當前循環到

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_22

。如果 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_29

,取 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_47

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_48

。那麼當前位就等於 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_48

。因為按理説當前位就該是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位未出現的最小數了,但 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏又有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_29

。這代表在第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_22

 位,還有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_24

 個比它小的數構成的排列排在它前面(

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位不動),又由於不能重複,所以取 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_47

 小數。別忘了 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_62

。還是拿 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02

 的例子,現在我們只知道 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_64

。當 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_65

 時,

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_67

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_68


luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_67

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

。當 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_73

 時,

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_76


luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_07


luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_81

。當 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_82

 時,

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_85


luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_11


luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_90

。當 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_91

 時,

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_67

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_94


luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_07

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_12

。當 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_99

 時,

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開

 裏面有 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_67

 個 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_102


luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_11

 位未出現的第

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 小數為 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_03

。得出 

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_02


實現

難點在求 

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_27

 到

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_28

 位未出現的最小數,這玩意

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_110

,想優化上線段樹 or 樹狀數組上倍增。(不過例題

luogu P3014 牛線 (康拓展開及其逆向操作)_#康託展開_21

 很小不需要,無所謂我會給出兩份代碼)

例題:P3014 [USACO11FEB] Cow Line S - 洛谷

P 操作就是逆展開。

luogu P3014 牛線 (康拓展開及其逆向操作)_#排列映射_112

,long long 的範圍是 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_113

 到 

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_114

。即差不多 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_115

,可以放心用。逆展開

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_110

 代碼(我用了 set,總時間複雜度

luogu P3014 牛線 (康拓展開及其逆向操作)_#算法_117

 可 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;
}

逆展開樹狀數組倍增 

luogu P3014 牛線 (康拓展開及其逆向操作)_#c++_118

做法,總時間複雜度 

luogu P3014 牛線 (康拓展開及其逆向操作)_#狀態壓縮_119


#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;
}