前言:
本身
我們先來看一下這個數列本身:
數列的前幾項為:1, 1, 2, 5, 14, 42, 132, 429, 1430, 4862。
請記住這些特殊的數字,在信息學競賽里許多題目都有這個數列的存在,可以在找規律時激發靈感。
意義:
卡特蘭數作為廣泛出現在OI中的一類特殊數列,其擁有廣泛的意義。
這里我們僅選擇一個經典例子作為講解
折線問題:
在平面上,從 \((0,0)\) 走至 \((n,n)\) ,每次只能向上或者向右走,不穿過 \(y = x\) 這條直線有多少種方案。
這里給出了一個可行方案。
\(Ans\):
我們考慮我們枚舉最后一個在 \(y = x\) 上的一個點 \((i,i)\) 可以考慮把問題規模縮小。
因為我們強制最后一段不能碰到 \(y = x\),所以我們發現在 \((i,i)\) 繼續向終點走的過程中,第一步只能向右走,到\((n,n)\)的最后一步只能向上走,中間的問題我們可以視作從 \((i,i + 1)\) 走到 \((n,n - 1)\) 不穿過 \(y = x - 1\)這條直線的方案數,我們平移一下:
我們發現中間這段的答案其實就是\(C_{n - i - 1}\)。
即可以求出\(C_n = \sum\limits_{i = 0}^{n - 1} C_iC_{n - i - 1}\)。
在眾多卡特蘭數所對應的題目如出棧順序,括號匹配中,縮小問題規模是一個很經典的做法。
但我們發現這樣直接計算,是一個 \(O(n^2)\) 的過程,其復雜度不盡如意。
我們思考是否能夠得到一個 \(O(n)\) 或者 \(O(1)\) 的遞推或者通項。
通項
考慮我們進行一個容斥的過程:
我們用所有路徑數減去不合法路徑的數量。
我們發現所有路徑即 \(\binom{2n}{n}\) 。
我們轉而思考不合法路徑的數量。
我們考慮把一條不合法路徑進行操作:
把一條不合法路徑在第一次觸碰到 \(y = x + 1\) 這條線的點設作 \(p\) ,我們把在 \(p\) 右側的在 \(y = x + 1\) 以下的部分全部沿 \(y = x + 1\)對稱到上方,我們發現這樣翻轉之后,所有可能出現的不合法路徑,和\((0,0) \to (n - 1,n + 1)\) 的路徑產生了一一映射的關系。
所以我們就有 \(C_n = \binom{2n}{n} - \binom{2n}{n - 1} = \frac{1}{n + 1}\binom{2n}{n}\)。
生成函數:
就和斐波那契數列一樣,我們一樣可以用處理數列的有力工具生成函數來處理卡特蘭數:
我們設 \(C_n\) 的生成函數為 \(H(x)\)。
我們發現卡特蘭數的遞推式和卷積形式相似,所以我們用卷積來構造 \(H(x)\)。
\(\begin{align*} H(x) &= \sum\limits_{n\geq 0}\sum\limits_{i = 0}^{n - 1}{C_iC_{n-i-1}x^n(n\geq 2)} \\ &= 1 + \sum\limits_{n\geq 1}\sum\limits_{i = 0}^{n - 1}C_i x^i C_{n - i - 1}x^{n - i - 1}x \\ &= 1 + x\sum\limits_{i\geq 0}C_ix^i\sum\limits_{n\geq 0}C_nx^n\\ &= 1 + xH^2(x) \end{align*}\)
我們解這個方程可得:
\(H(x) = \frac{1\pm \sqrt{1 - 4x}}{2x}\)。
我們需要選擇哪一個根呢。
我們進行分子有理化:
\(H(x) = \frac{2}{1\pm \sqrt{1 - 4x}}\)。
我們發現當我們選擇\(\frac{2}{1 - \sqrt{1 - 4x}}\)時,代入\(x = 0\)則會發現\(H(0) = 0\)的條件不符合的情況。
所以我們選擇\(H(x) = \frac{1 + \sqrt{1 - 4x}}{2x}\)。
但我們發現卡特蘭數和斐波那契的不同之處,這個\(H(x)\)的封閉形式,並不是一個多項式的形式。
所以我們考慮需要先展開 \(\sqrt{1 - 4x}\) :
我們使用二項式定理:
\(\begin{align*} (1 - 4x) ^ {\frac{1}{2}} &= \sum\limits_{n\geq 0}\binom{\frac{1}{2}}{n}(-4x)^n \\ &= 1 + \sum\limits_{n\geq 1}\frac{(\frac{1}{2}^ \underline{n})}{n!}(-4x)^n \\ \end{align*}\)
我們有\((\frac{1}{2})^\underline{n} = \frac{(-1)^{n -1}\ \ (2n - 2)!}{2^{2n - 1}\ \ (n - 1)!}\)。
由於這個柿子的化簡並不是我們討論的主要內容,有需要可以轉:化簡過程。
我們把這個柿子帶回原柿子,直接化簡可以得到:
\((1-4x)^{\frac{1}{2}} = 1 + \sum\limits_{n \geq 1}{\binom{2n-1}{n}\frac{1}{2n - 1}2x^n}\)。
再帶回原柿子。
\(H(x) = \sum\limits_{n\geq 0}\binom{2n}{n}\frac{1}{n + 1}x^n\)。
於是我們得到了通項。
\([x^n]H(x) = \frac{1}{n + 1}\binom{2n}{n}\)。
我們可以在 \(O(n)\) 次預處理的情況下,\(O(1)\) 回答一個詢問。
例題
[SCOI2010]生成字符串
我們考慮到其實這個任意前綴 1 的個數都小於前綴 0 的個數,實際上和我們的在二維平面上游走不穿過 \(y = x\) 這條線的條件是等價的。
那么我們可以把題目轉變為從 \((0,0)\) 走到 \((n,m)\) 處,不穿過 \(y = x\) 這條直線的方案數。
依照上面我們所做的翻轉理論,其實即翻轉完的終點變換為了 \((m - 1,n + 1)\) 。
所以答案為 \(\binom{n + m}{n} - \binom{n + m}{m - 1}\)。
#include<iostream>
#include<cstdio>
#define ll long long
#define N 1000005
#define mod 20100403
ll s[N << 1],inv[N << 1];
inline ll pow(ll a,ll b){
ll ans = 1;
while(b){
if(b & 1)ans = a * ans % mod;
a = a * a % mod;
b >>= 1;
}
return ans;
}
inline ll C(ll x,ll y){
return s[x] * inv[y] % mod * inv[x - y] % mod;
}
int main(){
ll n,m;
scanf("%lld%lld",&n,&m);
s[0] = 1;
for(int i = 1;i <= n + m;++i)
s[i] = s[i - 1] * i % mod;
inv[n + m] = pow(s[n + m],mod - 2);
for(int i = n + m - 1;i >= 0;--i)
inv[i] = inv[i + 1] * (i + 1) % mod;
std::cout<<(C(n + m,n) - C(n + m,m - 1) + mod) % mod<<std::endl;
}
[AHOI2012]樹屋階梯
求用 \(n\) 個任意大小的矩形,覆蓋高度為 \(n\) 的階梯的方案數。
我們思考強制使用 \(n\) 個這個矩形的條件能夠轉化成什么對我們有利的條件:
我們在左下角這個矩形,一定右上角是某個拐點,否則我們會發現,我們需要額外使用一個矩形去覆蓋這個拐點,\(n\) 個拐點對應 \(n\) 個矩形,那么這樣最少也需要 \(n + 1\) 個。
那么我們就有了一個可以縮小問題規模的方案。
我們每次對這個階梯狀物,進行一個枚舉覆蓋左下角這個矩形的右上角是哪一個拐點,不妨設為從上到下第 \(x\) 個。
所以這個矩形上方有一個 \(x - 1\) 階狀物需要覆蓋,右邊有一個 \(n - x\) 階狀物需要處理。
所以答案為 \(C_n = \sum\limits_{i = 0}^{n -1}{C_iC_{n - i - 1}}\) 即 卡特蘭數。
a=input()
c=1
for num in range (a+2,a*2+1):
c=c*num
for num in range (1,a+1):
c=c/num
print (c)
[TJOI2015]概率論
我們考慮期望的典型操作:
我們統計所有的可能出現的二叉樹的數量 \(f_n\),以及所有二叉樹的葉子總個數 \(g_n\)。
那么我們考慮前者,我們枚舉根節點左兒子的數量可以知道 \(f_n = \sum\limits_{i = 0}^{n - 1}{f_if_{n - i - 1}}\) 即卡特蘭數。
那么后面這個 \(g_n\) 怎么計算呢。(考慮打表並猜測並證明)。
\(g_n = nf_{n - 1}\)
考慮證明。
我們思考一顆 \(n\) 元樹,有 \(k\) 個葉子節點,那么我們把這個 \(k\) 個葉子分別去掉,都會得到一顆 \(n - 1\) 元樹,我們稱得到的 \(n - 1\) 元樹 \(\alpha\) 做了一次貢獻。
那么我們只要對所有的 \(\alpha\) 的貢獻之和就行了。
那么我們思考一下有多少個位置可以給我們放一個新的葉子節點獲得一個 \(n\) 元樹 \(\beta\)。
我們從度數角度考慮:
添加一個葉子后, \(dep(\beta) = dep(\alpha) + 1\) 。
一顆 \(\alpha\) 有 \(n - 1\)個點,其度數顯然為 \(2(n - 2)\)。
我們把其所有葉子部位都補上,則有這 \(n - 1\) 個點的度數為 \(3(n - 2) + n - 2\),即除了根節點是 2 度,其他點都是 3 度,每添一個葉子節點,度數只會增加 1 ,所以能添 \(n\) 個葉子節點。
所以有 \(g_n = n * f_{n - 1}\)。
所以直接計算答案 \(\frac{n * f_{n - 1}}{f_n} = \frac{n(n + 1)}{2(2n - 1)}\)。
#include <cstdio>
int main() {
double n;
scanf("%lf", &n);
printf("%.12f", n * (n + 1) / (2 * (2 * n - 1)));
return 0;
}
[NOI2018] 冒泡排序
我們考慮轉化條件:
好排列等同於序列中不存在一個長度大於等於 \(3\) 的下降子序列。
首先考慮排列如何達到交換下界。
單獨考慮排列的一個數,對於其目標位置,我們知道他一定往目標去,那么對於排列\(2,1\),\(2\)要到后面,\(1\)要到前面,所以交換不會浪費次數。
但是如果 \(a_i,a_j,a_k(a_i > a_j > a_k)\) 那么 \(a_i\) 到后面,和\(a_k\) 到前面,中間 \(a_j\) 會浪費次數。
我們考慮當我們忽略字典序條件我們怎么做呢。
我們設 \(f_{i,j}\),為選了 \(i\) 個數,最大值為 \(j\) 的方案數,所以我們要么選一個更大的,要么選一個小的。
我們考慮不能出現三元以上的下降。
所以我們轉移為:
\(f_{i,j} = \sum\limits_{j}^{k=i - 1}f_{i - 1,k}\)。
我們可以推出\(f_{i,j} = f_{i - 1,j} + f_{i,j - 1}(j \leq i)\)。
那么又轉化為了平面游走問題。
我們有\(f_{i,j} = \binom{i + j}{i} - \binom{i + j}{j + 1}\)。
那么我們怎么處理字典序問題呢。
我們枚舉位置 \(i\) ,前面的都和 \(p\) 相同,第 \(i\) 個數大於\(p_i\),然后將方案數加起來。
令 \(mx = \max\limits_{j = 1}^{i - 1}{p_j},mi\) 為當前最小的可以填的數。
那么我們重新定義 \(f_{i,j}\) 為從 \((i,j) \to (n,n)\) 的方案數,這里可以理解為我們定義 \(f_{i,j}\) 轉為定義了前 \(i\) 個數 \(mx = j\),后面 \(n - j\) 個數填的方案數,可以看做是對於二元 \(f\) 做了一個后綴和,建議讀者結合二維平面思考。
- 如果 \(p_i\) = \(mi\),顯然我們只能填 \(x > mx\) 的數方案:\(f(i,mx + 1)\)。
- 如果 \(mi < p_i < mx\) ,顯然我們只能填 \(x > mx\) 的數,但我們思考這樣一定會有\(mx,p_i,mi\)的一個三元序列,所以此時無解。
- 如果\(p_i \geq mx\),我們填一個 \(x > p_i\)的數,那么方案數 \(f(i,p_i + 1)\)。
#include<iostream>
#include<cstdio>
#define ll long long
#define N 2000005
#define mod 998244353
ll s[N << 1 + 5],inv[N << 1 + 5];
inline ll pow(ll a,ll b){
ll ans = 1;
while(b){
if(b & 1)ans = a * ans % mod;
a = a * a % mod;
b >>= 1;
}
return ans;
}
inline ll C(ll x,ll y){
return s[x] * inv[y] % mod * inv[x - y] % mod;
}
ll n,p[N];
ll T;
bool in[N];
ll fi(ll x,ll y){
if(x > y || y > n)return 0;
return (C((n << 1) - x - y,n - x) - C((n << 1) - x - y,n - x + 1) % mod + mod) % mod;
}
inline void solve(){
scanf("%lld",&n);
for(int i = 1;i <= n;++i)
scanf("%lld",&p[i]),in[i] = 0;
ll mx = 0,mi = 1,ans = 0;
for(int i = 1;i <= n;++i){
mx = std::max(mx,p[i]);
ans = (ans + fi(i - 1,mx + 1)) % mod;
in[p[i]] = 1;
while(in[mi])mi ++ ;
if(mi < p[i] && p[i] < mx)break;
}
std::cout<<ans<<std::endl;
}
int main(){
s[0] = 1;
for(int i = 1;i <= N << 1;++i)
s[i] = s[i - 1] * i % mod;
inv[N << 1] = pow(s[N << 1],mod - 2);
for(int i = (N << 1) - 1;i >= 0;--i)
inv[i] = inv[i + 1] * (i + 1) % mod;
scanf("%lld",&T);
while(T -- ){
solve();
}
}
總結
本文旨在帶領讀者發現一些在卡特蘭數列中蘊含的內在,從這么多例題來看,卡特蘭數數列本身並沒有多少可擴展之物,其也很容易從打表暴力等方式看出來,但在其推導過程中的關鍵思想:容斥,通過某種操作縮小問題規模,找到關鍵性質,將一個抽象問題轉化為幾何問題對我們仍有啟發意義,我們更應該關注的是在數列背后的一些樂趣,而非單純用打表法做出題目。