【學習筆記】卡特蘭數
基本概念
問題引入:網格行走問題
在一個平面直角坐標系內,你位於 \((0, 0)\),你想要走到 \((n, n)\) (\(n\geq 1\))。每步只能向右或向上走一單位長度。要求在任意時刻,你所處的坐標 \((x, y)\) 滿足 \(x\geq y\)(也就是不能越過第一象限角平分線)。求有多少種合法的方案。
做法:如果不考慮“不能越過第一象限角平分線”的要求,那么總方案數顯然是 \({2n\choose n}\),也就是在總共 \(2n\) 步中,任選出 \(n\) 步向上走(剩下的 \(n\) 步向右走)。
下面考慮減去不合法的方案。
如果一種方案不合法,那這條路徑上至少會有一個點,碰到了直線 \(y = x + 1\)。假設路徑第一次碰到 \(y = x + 1\) 的點為 \(p\)。將 \(p\) 之后的路徑(即從 \(p\) 到 \((n, n)\) 的路徑)關於直線 \(y = x + 1\) 做對稱。
如下圖,綠線即 \(y = x + 1\),紅線為原路徑越過第一象限角平分線后的部分,藍線為關於 \(y = x + 1\) 對稱后的結果。
發現任何一條不合法的路徑,對稱后都唯一對應一條從 \((0, 0)\) 到 \((n - 1, n + 1)\) 的路徑。並且,任何一條從 \((0, 0)\) 到 \((n - 1, n + 1)\) 的路徑,也唯一對應了一條不合法的原路徑(從第一次經過 \(y = x + 1\) 的點開始,對稱回來,就能得到原路徑了)。因此,二者之間是一一映射的關系。
所以,不合法的路徑數量,就等於從 \((0, 0)\) 到 \((n - 1, n + 1)\) 的路徑數量,即 \({2n\choose n + 1}\)。
所以答案就等於:
示意圖:

定義:卡特蘭數
它的前幾項(從 \(c_0\) 開始)是:\(1\), \(1\), \(2\), \(5\), \(14\), \(42\), \(132\), \(429\), \(1430\), \(4862\) ...
引理1:卡特蘭數的另一種形式
證明:
\[\begin{align} c_n &= {2n\choose n} - {2n\choose n + 1}\\ &= \frac{(2n)!}{n!n!} - \frac{(2n)!}{(n + 1)!(n - 1)!}\\ &= \frac{(2n)!\cdot (n + 1)!(n - 1)! - (2n)!\cdot n!n!}{n!n!(n + 1)!(n - 1)!}\\ &= \frac{(2n)!(n - 1)!n!\cdot ((n + 1) - n)}{n!n!(n + 1)!(n - 1)!}\\ &= \frac{(2n)!}{n!(n + 1)!}\\ &= \frac{1}{n + 1}\cdot \frac{(2n)!}{n!n!}\\ &= \frac{{2n\choose n}}{n + 1} \end{align} \]
引理2:卡特蘭數的遞推式
令 \(c_{0} = 1\),則對任意 \(n > 0\),有:
證明:
考慮上述的網格行走問題,我們枚舉路徑里(除起點外)第一次碰到直線 \(y = x\) 的點,設它的坐標為 \((i, i)\) (\(1\leq i\leq n\))。
那么從 \((0, 0)\) 走到 \((i, i)\) 的方案數,就相當於從 \((1, 0)\) 走到 \((i, i - 1)\) 且不越過直線 \(y = x - 1\) 的方案數(因為我們要保證 \((i, i)\) 是第一次碰到 \(y = x\),所以之前不能碰),即 \(c_{i - 1}\)。
從 \((i, i)\) 走到 \((n, n)\) 的部分,方案數顯然是 \(c_{n - i}\)。
所以每個 \(i\) 貢獻的方案數就是 \(c_{i - 1} \cdot c_{n - i}\)。
小練習:用生成函數方法,從【遞推式】推出【定義式】。
幾種常見的實際意義
- \(n\) 對括號的合法括號序列數。把左括號看做向右走,右括號看做向上走,則等價於上述的網格行走問題。
- \(n\) 個數入棧、出棧(以固定順序入棧,在任意棧非空的時刻可以選擇彈出一個數)得到的排列數。入棧即向右走,出棧即向上走,等價於網格行走問題。
- \(n\) 個節點的二叉樹數量。觀察上述遞推式,相當於枚舉左子樹大小為 \(i - 1\),右子樹大小為 \(n - i\)。
- \(n\) 層的階梯切割為 \(n\) 個矩形的切法數(見「AHOI2012」樹屋階梯)。
再探網格行走問題
將【網格行走問題】中的終點從 \((n, n)\) 改為 \((n, m)\),保證 \(n\geq m\)。仍然要求在任意時刻你所處的坐標 \((x, y)\) 滿足 \(x\geq y\)。求有多少種合法的方案。
做法:仍然考慮在第一次碰到直線 \(y = x + 1\) 時,將此后的路徑關於 \(y = x + 1\) 對稱。發現不合法的路徑,與從 \((0, 0)\) 到 \((m - 1, n + 1)\) 的路徑一一映射。所以答案就是
詳見此題:「SCOI2010」生成字符串。
例題1:「HNOI2009」有趣的數列
題目大意:
我們稱一個長度為 \(2n\) 的數列是有趣的,當且僅當該數列滿足以下三個條件:
- 它是從 \(1 \sim 2n\) 共 \(2n\) 個整數的一個排列 \(\{a_n\}_{n=1}^{2n}\);
- 所有的奇數項滿足 \(a_1 < a_3 < \dots < a_{2n-1}\),所有的偶數項滿足 \(a_2 < a_4 < \dots < a_{2n}\);
- 任意相鄰的兩項 \(a_{2i-1}\) 與 \(a_{2i}\) 滿足:\(a_{2i-1}<a_{2i}\)。
例如,\(n = 3\) 時共有 \(5\) 個有趣的數列:\((1,2,3,4,5,6)\), \((1,2,3,5,4,6)\), \((1,3,2,4,5,6)\), \((1,3,2,5,4,6)\), \((1,4,2,5,3,6)\)。
對於給定的 \(n\),請求出有多少個不同的長度為 \(2n\) 的有趣的數列。答案對一個給定的數 \(p\) 取模(注意,\(p\) 不一定是質數)。
數據范圍:\(1\leq n\leq 10^{6}\),\(1\leq p\leq 10^{9}\)。
考慮將數字 \(1, 2,\dots, 2n\) 依次填入排列,使結果是有趣的。那么,我們每次一定會選擇【最小的空奇數位】或【最小的空偶數位】。因為只有這樣才能使得奇數項和偶數項分別遞增。
但此時仍然不一定滿足【\(\forall i: a_{2i-1}<a_{2i}\)】的要求。考慮如果存在 \(a_{2i - 1} > a_{2i}\),說明 \(2i - 1\) 這個位置上的數,填的時間比 \(2i\) 位置遲。也就是第 \(i\) 個奇數位填的時間比第 \(i\) 個偶數位遲。我們要避免這種情況,等價於保證在任意時刻【奇數位上的數的數量】\(\geq\)【偶數位上的數的數量】。
把【在奇數位填一個數】看做向右走一步,【在偶數位填一個數】看做向上走一步,那么原問題等價於【網格行走問題】。所以答案就是卡特蘭數,即:\(\frac{{2n\choose n}}{n + 1}\)。
本題的另一個難點是,模數不一定是質數,不方便求逆元。考慮如何不使用除法。
考慮求出每個質數對答案的貢獻(幾次冪),再相乘。分別算出分子、分母里每個質數的次冪,然后相減即可(除法被轉化為了減法!)。計算每個質數的次冪,我的做法是枚舉所有 \(i\),並分解質因數。暴力分解質因數,單次的復雜度是 \(\mathcal{O}(\sqrt{n})\) 的,太慢了。可以先用線性篩預處理出每個數的最小質因子,這樣在分解質因數時,可以省去不必要的枚舉,單次分解的復雜度就是每個數的質因子數量,是 \(\mathcal{O}(\log n)\) 的,總時間復雜度 \(\mathcal{O}(n\log n)\)。
參考代碼
// problem: P3200
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXM = 2e6;
int n, MOD;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
inline int pow_mod(int x, int i) {
int y = 1;
while (i) {
if (i & 1) y = (ll)y * x % MOD;
x = (ll)x * x % MOD;
i >>= 1;
}
return y;
}
int p[MAXM + 5], cnt;
bool v[MAXM + 5];
int minp[MAXM + 5];
int f[MAXM + 5];
int main() {
cin >> n >> MOD;
int m = 2 * n;
for (int i = 2; i <= m; ++i) {
if (!v[i]) {
p[++cnt] = i;
minp[i] = i;
}
for (int j = 1; j <= cnt && p[j] * i <= m; ++j) {
v[p[j] * i] = 1;
minp[p[j] * i] = p[j]; // 最小質因子
if (i % p[j] == 0) {
break;
}
}
}
for (int i = n + 2; i <= m; ++i) {
int x = i;
while (x != 1) {
int y = minp[x];
while (x % y == 0) {
f[y]++;
x /= y;
}
}
}
for (int i = 2; i <= n; ++i) {
int x = i;
while (x != 1) {
int y = minp[x];
while (x % y == 0) {
f[y]--;
x /= y;
}
}
}
int ans = 1;
for (int i = 2; i <= m; ++i) {
if (!v[i]) {
assert(f[i] >= 0);
ans = (ll)ans * pow_mod(i, f[i]) % MOD;
}
}
cout << ans << endl;
return 0;
}
例題2:「NOI2018」冒泡排序
題目大意:
冒泡排序算法:
輸入:一個長度為 n 的排列 p[1...n]
輸出:p 排序后的結果。
for i = 1 to n do
for j = 1 to n - 1 do
if(p[j] > p[j + 1])
交換 p[j] 與 p[j + 1] 的值
可以證明,交換次數的一個下界是 \(\frac{1}{2}\sum_{i = 1}^{n}|i - p_i|\)。
稱一個長度為 \(n\) 的排列是好的,當且僅當對它進行冒泡排序的交換次數恰好等於 \(\frac{1}{2}\sum_{i = 1}^{n}|i - p_i|\)。
給定一個長度為 \(n\) 的排列 \(q\)。求字典序嚴格大於 \(q\) 的好的排列數。答案對 \(998244353\) 取模。
數據范圍:每個測試點有 \(5\) 組測試數據,每組測試數據滿足 \(1\leq n\leq 6\times 10^5\),整個測試點滿足 \(\sum n\leq 2\times 10^6\)。
發現,一個排列是好的,當且僅當不存在長度 \(\geq 3\) 的下降子序列。
考慮逐位構造一個好的排列,現在填到位置 \(i\),前 \(i-1\) 位的最大值為 \(\mathrm{mx}\)。則第 \(i\) 位要么填任意一個 \(> \mathrm{mx}\) 的數,要么填 $ < \mathrm{mx}$ 的最小的數(否則就一定會出現長度為 \(3\) 的下降子序列)。
於是想到 DP。設 \(\mathrm{dp}(i,j)\) 表示前 \(i\) 位的最大值是 \(j\) 的情況下,第 \(i+1\) 到第 \(n\) 位的填數方案。這樣我們可以從后往前轉移(或者用記憶化搜索實現),即:
特別地,如果 \(j < i\),則 \(\mathrm{dp}(i, j) = 0\)。
為什么要把 DP 數組定義成“第 \(i\) 位之后的填數方案”呢?因為這樣便於我們處理字典序的問題。我們統計答案時,枚舉從第 \(i\) 位開始,字典序第一次大於輸入的排列 \(q\)(前 \(i-1\) 位全部和 \(q\) 相等)。設 \(\mathrm{mx}_i=\max_{j=1}^{i}q_j\),則:
答案就是所有 \(\mathrm{ans}_i\) 之和。
這樣暴力 DP 是 \(\mathcal{O}(n^3)\) 的,用后綴和優化可做到 \(\mathcal{O}(n^2)\)。
繼續觀察這個 DP。發現 \(\mathrm{dp}(i, j)\) 就相當於在一個二維平面上,從點 \((i, j)\) 走到點 \((n, n)\) 的方案數。同時我們有一些要求:
- 每輪必須先向右走一步(也就是 \(i\to i + 1\))。
- 然后可以向上走若干步,或不向上走(也就是 \(j\to k\), \(k\geq j\))。
- 每輪結束時,需保證所在位置 \((i, j)\) 滿足 \(i\leq j\)。
- 如此進行 \(n - i\) 輪之后,恰好到達點 \((n, n)\)。
稱這樣的 \(n - i\) 個“輪”,為一個“方案”。我們要計算滿足上述要求的“方案”的數量。
直接對“方案”計數,其實就是上述 DP 的過程了。但我們要優化它,就必須跳出這個思路的局限。把方案里的所有“輪”拆散了看,它就是一條從 \((i, j)\) 走到 \((n, n)\) 的路徑(這里和后文中所有“路徑”都是指:每步只能向上或向右走一格),其中每向右走一步,就相當於開始了新的一輪。並且任意一條路徑一定恰有 \(n - i\) 步是向右走的,因此我們不需要刻意地去划分出輪次,直接對路徑計數即可。
具體來說,路徑需要滿足如下要求:
- 路徑的第一步必須是向右走的:也就是 \((i, j)\to (i + 1, j)\),而不能是 \((i, j)\to (i, j + 1)\)。
- 在原來的“方案”里,要求每輪結束時滿足 \(i\leq j\),但在過程中(比如說先向右走了一步,還沒向上走之前)是不一定的。實際上,要求可以轉化為:路徑里不能存在 \((i, j) \to (i + 1, j)\) 且 \(i > j\),因為這一步是向右走的,意味着 \((i, j)\) 這一輪已經結束了。所以要求可以進一步轉化為,整個路徑中,不能存在 \(i - j\geq 2\)。
考慮如何統計滿足上述兩個要求的路徑數。第 1 個要求很好實現,我們把起點設為 \((i + 1, j)\) 即可!
第 2 個要求相當於,整個過程里,不能碰到直線 \(y = x - 2\)。類比卡特蘭數的推導方法,考慮用總數減去不合法的路徑數。
- 總數即從 \((i + 1, j)\) 走到 \((n, n)\) 的路徑數,顯然是 \({n - (i + 1) + n - j\choose n - (i + 1 )} = {2n-i-j-1\choose n - i - 1}\)。
- 不合法即碰到了直線 \(y = x - 2\)。我們在它第一次碰到時,將路徑關於 \(y = x - 2\) 對稱。那么【不合法的路徑】和【從 \((i + 1, j)\) 走到 \((n + 2, n - 2)\) 的路徑】形成了一一映射。所以不合法的路徑數等於【從 \((i + 1, j)\) 走到 \((n + 2, n - 2)\) 的路徑數】,即 \({(n + 2) - (i + 1) + (n - 2) - j\choose (n + 2) - (i + 1)} = {2n-i-j-1\choose n - i + 1}\)。
綜上所述,\(\mathrm{dp}(i, j) = {2n - i - j - 1\choose n - i - 1} - {2n - i - j - 1\choose n - i + 1}\)。按之前的方法,直接統計答案即可。
注意,如果 \(q\) 的前 \(i\) 位已經存在不合法的情況(不符合“第 \(i\) 位要么填一個 \(> \mathrm{mx}_{i-1}\) 的數,要么填 \(< \mathrm{mx}_{i-1}\) 的最小的數”這條規則),要及時 \(\texttt{break}\)。
時間復雜度 \(\mathcal{O}(n)\)。
參考代碼
實際提交時請使用讀入優化,詳見本博客公告。
// problem: LOJ2719
#include <bits/stdc++.h>
using namespace std;
#define mk make_pair
#define fi first
#define se second
#define SZ(x) ((int)(x).size())
typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;
template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }
const int MAXN = 6e5, MOD = 998244353;
inline int mod1(int x) { return x < MOD ? x : x - MOD; }
inline int mod2(int x) { return x < 0 ? x + MOD : x; }
inline void add(int &x, int y) { x = mod1(x + y); }
inline void sub(int &x, int y) { x = mod2(x - y); }
inline int pow_mod(int x, int i) {
int y = 1;
while (i) {
if (i & 1) y = (ll)y * x % MOD;
x = (ll)x * x % MOD;
i >>= 1;
}
return y;
}
int fac[MAXN * 2 + 5], ifac[MAXN * 2 + 5];
inline int comb(int n, int k) {
if (n < 0 || k < 0 || n < k) return 0;
return (ll)fac[n] * ifac[k] % MOD * ifac[n - k] % MOD;
}
void facinit(int lim = MAXN) {
fac[0] = 1;
for (int i = 1; i <= lim; ++i) fac[i] = (ll)fac[i - 1] * i % MOD;
ifac[lim] = pow_mod(fac[lim], MOD - 2);
for (int i = lim - 1; i >= 0; --i) ifac[i] = (ll)ifac[i + 1] * (i + 1) % MOD;
}
int n, a[MAXN + 5];
bool vis[MAXN + 5];
inline int f(int i,int j){
if(i > j) return 0;
if (i == n && j == n) return 1;
return mod2(comb(n + n - i - j - 1, n - i - 1) - comb(n + n - i - j - 1, n - i + 1));
}
void solve_case() {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
vis[i] = 0;
}
int ans = 0;
for (int i = 1, mx = 0, pos = 1; i <= n; ++i) {
// 枚舉從第 i 個位置起, 新序列大於原序列
mx = max(mx, a[i]);
add(ans, f(i - 1, mx + 1));
// pos 是最小的沒有填過的值
if (a[i] < mx && a[i] != pos)
break; // 已經不合法了!
vis[a[i]] = 1;
while (vis[pos]) ++pos;
}
cout << ans << endl;
}
int main() {
freopen("inverse.in","r",stdin);
freopen("inverse.out","w",stdout);
facinit(MAXN * 2);
int T; cin >> T; while (T--) {
solve_case();
}
return 0;
}
一道類似的題目
ARC068D Solitaire(加強版)
該加強版見於六校聯考,目前不公開,無法提交。
題目大意:
給定正整數 \(n\),和一個初始為空的雙端隊列。將 \(1,2,\dots,n\) 順次插入該雙端隊列的任何一端。再以任意順序從兩端彈出數形成一個長為 \(n\) 的排列。對於一個排列,若存在一種操作方式得到它,則稱它是好的。
現在有 \(q\) 次詢問,每次給定 \(n, m(1\le m\le n)\),請求出長度為 \(n\) 且第 \(m\) 項為 \(1\) 的好的排列的個數,對 \(998244853\) 取模。
數據范圍:\(1\leq n\le 3\times 10^6\),\(1\leq q\le 5\times 10^5\)。
稱通過把 \(1\dots n\) 依次從兩側加入得到的排列為一個“雙端隊列”。發現一個排列是雙端隊列當且僅當其從開頭到 \(1\) 遞減,從 \(1\) 到結尾遞增。
按照題目的定義,一個好的排列,指它能夠通過從一個雙端隊列兩側彈出數字得到。發現一個排列是好的,當且僅當它能被拆分為兩個子序列 \(A, B\),且 \(A + \mathrm{reverse}(B)\) 是一個雙端隊列。這里 \(A\) 就代表從左邊彈出的數,\(B\) 就代表從右邊彈出的數。
不妨假設 \(1\) 是從左邊彈出的。如果它是從右邊彈出的,則把雙端隊列反轉一下即可。換句話說,我們通過 \(1\) 被彈出的方向,來定義“左”和“右”。
那么 \(A\) 應該先遞減,減到 \(1\),然后遞增;\(B\) 應該一直遞減。且 \(B\) 里的所有數,應該都大於 \(A\) 中在 \(1\) 后面的數。
我們先假設,\(1\) 是 \(A\) 里的最后一個數。也就是結果序列的 \(m + 1\dots n\) 位置全部划給 \(B\)。假設此時 \(A\), \(B\) 已經確定。然后枚舉一個 \(k\in[0, n - m]\), 把 \(B\) 里前 \(k\) 小的數還給 \(A\)。相當於本來 \(B\) 里前 \(n - m\) 小的數字,是按從大到小填在 \(m + 1\dots n\) 這些位置上,現在我們要從中選出 \(k\) 個位置,把前 \(k\) 小的數從小到大填在這 \(k\) 個位置上,其他數從大到小填在剩下的位置上。這么做的方案數是:
其中,\({n - m\choose k}\) 表示選出還給 \(A\) 的這 \(k\) 個位置。減去 \({n - m - 1\choose k - 1}\),是如果位置 \(n\) 出現在這 \(k\) 個位置當中,那么同樣的排列在 \(k - 1\) 時已經被統計過了(也就是說,如果位置 \(n\) 恰好填第 \(k\) 小的數,則把它划分給 \(A\) 或划分給 \(B\) 都是合法的,所以這種排列會被計算兩次,要減掉)。
注:后來讀了題解,發現一種更簡單的理解方法。考慮 \(1\) 被從雙端隊列里彈出后,隊列里剩余 \(n - m\) 個數。每個數都可以選擇從左邊彈出或從右邊彈出,所以方案數是 \(2^{n - m - 1}\)。
現在我們已經會處理后半部分了。接下來只需要考慮【\(1\) 是 \(A\) 里的最后一個數】的划分方案,把這個方案乘以 \(2^{n - m - 1}\) 就是答案(注意特判 \(m = n\) 時不用乘)。問題轉化為:求一個排列,滿足它能被划分為兩個單調減序列,且位置 \(m\) 上是 \(1\)。
定義一個排列是優美的,當且僅當它能被划分為兩個單調減序列。根據 \(\text{Dilworth}\) 定理,一個排列是優美的,當且僅當它不存在長度 \(\geq 3\) 的上升子序列。
對排列 \(p\),定義 \(p^{-1}\) 也是一個排列,滿足 \(p^{-1}_{p_i} = i\),也就是把原排列里的“數值”和“位置”互換了。發現一個排列 \(p\) 是優美的,等價於 \(p^{-1}\) 是優美的。
所以問題轉化為,求位置 \(1\) 上是 \(m\) 的、不存在長度 \(\geq 3\) 的上升子序列的,排列數量。
這個問題幾乎就是 NOI2018 冒泡排序,只不過把下降改成了上升。方法是一樣的:通過 DP 和卡特蘭數,可以推出,答案就是從 \((2, m)\) 走到 \((n, 1)\)(每步只能向右或向下),且不碰到直線 \(y = -x + n + 3\) 的路徑數,是 \({n - 2 + m - 1\choose n - 2} - {n + m - 3\choose n}\)。推導過程留給讀者自行完成。
