组合数学学习笔记
卡特兰数及反射容斥
卡特兰数有一些比较常见的模型,大多题目都是比较板的。
卡特兰数主要可以用于解决对于前缀有一定约束时的计数问题(括号序列前缀和非负)。
一些期望有关题目也可以转为计数,再除去总方案数。
卡特兰数的常见公式
卡特兰数前几项为 : \(1,1,2,5,14,42,132,429 ...\),详见oeis.
卡特兰数通项公式为 : $C_n = \binom{2n}{n} - \binom{2n}{n-1} $.
根据通项公式,通分后加减可得$ C_n = \frac{ (2n)! }{ (n+1)! \cdot n! } = \frac{ \binom{2n}{n} }{n+1} $.
卡特兰数的递推公式为 :\(C_n = \sum_{i=1}^{n} C_{i-1} \times C_{n-i}\)。
根据其通项公式,两项做商可以推出另外一个递推公式:$ C_n = \frac{4n-2}{n+1} C_{n-1} $.
对于这些式子的组合意义,会在应用中说明。
卡特兰数的常见模型
合法括号序列个数
结论:长度为\(2n\)的合法括号序列个数为\(C_n\)。
一个合法序列位置 1 一定是左括号,可以考虑枚举与 1 位置匹配的右括号的位置\(2i\)(合法序列与 1 匹配的右括号位置一定为偶数),前后两端转化为两个本质相同的子问题,同时这样也就得到了卡特兰数的递推公式,$ C_n = \sum_{i=1}^{n} C_{i-1} \times C_{n-i} $。
大致如下图。

出栈序列个数
结论:有\(n\)个元素的入栈序列所对应的合法出栈序列个数为\(C_n\)。
将入栈视为左括号,出栈视为右括号,与括号序列本质相同。
合法二叉树个数
结论:有\(n\)个节点的二叉树有\(C_n\)种。
考虑枚举根节点左右子树大小,然后转化为本质相同子问题,就得到了递推公式。
凸多边形三角剖分数量
结论:有\(n+2\)条边的凸多边形的三角剖分数量是\(C_n\)。
考虑先固定两个点\(P_1\),\(P_2\),然后枚举这两个点与哪个点组成最后的三角形,其他转化成本质相同子问题,符合递推公式。
园内弦不相交计数
结论 :有\(2n\)个点的圆把这些点对链接起来且弦之间两两不相交的方案数为\(C_n\)。
圆上的点按编号顺序排列,考虑 1 号一定要选择一个偶数点,接下来与括号序列匹配本质相同。
格路计数问题
一维版本
反射容斥的基础应用。
数轴上初始位置是原点,每次可以向左或右走一步,一共\(2n\)步,最终回到原点,且这期间不能走到负半轴,求合法的方案数。
考虑走到负半轴意味着一定走到了点 -1 ,那么以点 -1 作为中心对称,则所有非法路径的终点变为了点 -2 ,那么也就意味着向左走了\(n+1\)步,合法方案数即\(C_n = \binom{2n}{n} - \binom{2n}{n+1}\).
典中典。
这个是卡特兰数最广为人知的模型,也是反射原理的基础应用。
考虑以下问题:在平面直角坐标系中从\((0,0)\)走到\((n,n)\),且不能越过直线$ y = x $(可以接触)的方案数。
考虑容斥,总方案数为 $ \binom{2n}{n} $.
非法方案数为越过直线$ y = x $ 且到达\((n,n)\)的方案数,那么其一定经过直线 $y = x + 1 $.
考虑关于直线$ y = x + 1 $ 对原路径作对称,那么新终点变为 \((n-1,n+1)\),如下图。

那么接下来只需要用总方案数减去非法方案即可,即$C_n = \binom{2n}{n} - \binom{2n}{n-1} $.
注意:如果问题不变,走到\((n,m)\),对称点为\((m-1,n+1)\),合法方案数为\(C_n = \binom{n+m}{n} - \binom{n+m}{m-1} = \binom{n+m}{n} - \binom{n+m}{n+1}\)。
卡特兰数相关例题
[HNOI2009] 有趣的数列
link.
注意到 \(a_1 = 1\) 是必然的,那么接下来的每个数按升序要么放在奇数位置,要么放在偶数位置。
同时必须要满足奇数位置的数已填的数量大于等于偶数位置已填的数量。
这就转化成了括号序列匹配问题,答案即为\(C_n\)。
注意模数不是质数,做除法操作时需要质因数分解,转化为质因子次幂的加减。
Code
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 2e18
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=1e6+5,M=1e6+5;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
bool vis[2*N];
int n,mod,p[N],Cnt,minn[2*N],cnt[2*N];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod,y>>=1;
}
return res%mod;
}
signed main(){
// ios::sync_with_stdio(0);
// cin.tie(0); cout.tie(0);
int tc=1;
while(tc--){
cin>>n>>mod;
for(int i=2;i<=2*n;i++){
if(!vis[i]) p[++Cnt]=i,minn[i]=i;
for(int j=1;j<=Cnt;j++){
int k=i*p[j];
if(k>2*n) break;
vis[k]=1,minn[k]=p[j];
if(i%p[j]==0) break;
}
}
for(int i=1;i<=n;i++) cnt[i]=-1;
for(int i=n+2;i<=2*n;i++) cnt[i]=1;
for(int i=2*n;i>=2;i--){
if(!vis[i]) continue;
cnt[minn[i]]+=cnt[i];
cnt[i/minn[i]]+=cnt[i];
}
int ans=1;
for(int i=2;i<=2*n;i++)
if(!vis[i]) ans=ans*qpow(i,cnt[i])%mod;
cout<<ans<<"\n";
}
return 0;
}
[SCOI2010] 生成字符串
link.
每一个 1 字符可以视为向右走,每一个 0 字符可以视为向上走,原问题转化为格路计数问题扩展至\((n,m)\)的版本。
答案即为$ ans = \binom{n+m}{n} - \binom{n+m}{n+1}$。
Code
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 2e18
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=2e6+5,M=1e6+5;
const int mod=20100403;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,m;
int jc[N],nijc[N];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod,y>>=1;
}
return res%mod;
}
inline int C(int n,int m){
return jc[n]*nijc[m]%mod*nijc[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
jc[0]=1,nijc[0]=1;
for(int i=1;i<=2e6;i++) jc[i]=jc[i-1]*i%mod,nijc[i]=qpow(jc[i],mod-2)%mod;
int tc=1;
while(tc--){
cin>>n>>m;
cout<<((C(n+m,n)-C(n+m,n+1))%mod+mod)%mod<<"\n";
}
return 0;
}
[ABC205E] White and Black Balls
link.
将黑球视为 +1 ,白球视为 -1,那么终点为$ m - n $。
限制为$ b + k \geq w $,移项得 $ b - w \geq -k $。
那么将问题转化为数轴的形式,即不能越过点 \(-k\)。
那么令$ -(k+1) $ 为对称中心,非法的终点为 $ n - m - 2(k+1) $。
设向右走了\(R\)步,向左走了\(L\)步。
则有\(\left\{ \begin{aligned} L + R &= n + m \\ R - L &= n - m - 2(k + 1) \end{aligned} \right.\)
解二元一次方程组得到向右走了 \(n - k - 1\) 步,向左走了 $ m + k + 1 $ 步。
那么非法步数就是$ \binom{n+m}{m+k+1} $。
则答案为$ \binom{n+m}{n} - \binom{n+m}{m+k+} $。
Code
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 2e18
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=2e6+5,M=1e6+5;
const int mod=1e9+7;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,m,k;
int jc[N],nijc[N];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod,y>>=1;
}
return res%mod;
}
inline int C(int n,int m){
if(n-m<0) return 0;
return jc[n]*nijc[m]%mod*nijc[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
jc[0]=1,nijc[0]=1;
for(int i=1;i<=2e6;i++) jc[i]=jc[i-1]*i%mod,nijc[i]=qpow(jc[i],mod-2)%mod;
int tc=1;
while(tc--){
cin>>n>>m>>k;
if(n>m+k) cout<<0<<"\n";
else cout<<((C(n+m,n)-C(n+m,m+k+1))%mod+mod)%mod<<"\n";
}
return 0;
}
CF1204E Natasha, Sasha and the Prefix Sums
link.
考虑枚举最大前缀和,那么答案就转化为了计数问题。
考虑如何算出每种对应的最大前缀和的出现次数。
若钦定最大前缀和为\(k\),那么把 1 看成向右走,-1 看成向左走,就转化为了ABC205E。
记\(f_k\)为最大前缀和为\(k\)时的答案,注意\(f_k\)表示的是最大前缀和$ \leq k$的所有方案数,统计答案时需要容斥。
则$ ans = \sum_{i = \max( 1,n - m ) }^{n} f_i - f_{i-1} $.
Code
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 2e18
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=2e6+5,M=1e6+5;
const int mod=998244853;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,m,k,f[N];
int jc[N],nijc[N];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod,y>>=1;
}
return res%mod;
}
inline int C(int n,int m){
if(n-m<0) return 0;
return jc[n]*nijc[m]%mod*nijc[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
jc[0]=1,nijc[0]=1;
for(int i=1;i<=2e6;i++) jc[i]=jc[i-1]*i%mod,nijc[i]=qpow(jc[i],mod-2)%mod;
int tc=1;
while(tc--){
cin>>n>>m; int ans=0;
for(int i=0;i<=n;i++) f[i]=((C(n+m,n)-C(n+m,m+i+1))%mod+mod)%mod;
for(int i=max(1ll,n-m);i<=n;i++){
int d=((f[i]-f[i-1])%mod+mod)%mod;
ans=(ans+i*d%mod)%mod;
}
cout<<ans<<"\n";
}
return 0;
}
水の三角(修改版)
link.
把三角图拍直,可以发现一共只有 3 种操作,\(( +1 , 0 ) , ( 0 , +1 ) , ( +1 , +1 )\)。
可以发现如果不考虑\(( +1 , +1 )\)操作,就是卡特兰数中的格路计数问题的板子。
发现这个\(( +1 , +1 )\)操作很烦,考虑枚举其操作次数来去掉它。
而\(( +1 , +1 )\)操作的本质是平移,枚举此操作数为\(k\),范围为\([0,\min(x,y)]\)。
则总步数为$ x + y - k $ ,插板法得走\(k\)步的方案数为 \(\binom{x + y - k}{k}\)。
同时新的终点变为\((x - k , y - k)\) ,套用格路计数问题的计数方法,方案数为 $ f_k = \binom{x + y - 2k}{ x - k } - \binom{x + y - 2k}{ x - k + 1} $。
则 $ ans = \sum_{k=0}^{\min(x,y)} \binom{x + y - k}{k} f_k $。
Code
#include <bits/stdc++.h>
#define int long long
#define ull unsigned long long
#define inf 2e18
#define eps 1e-9
#define ls 2*k
#define rs 2*k+1
using namespace std;
const int N=2e6+5,M=1e6+5;
const int mod=998244353;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int n,pos=2e6;
int jc[N],nijc[N],sum[N];
inline int qpow(int x,int y){
int res=1;
while(y){
if(y&1) res=res*x%mod;
x=x*x%mod,y>>=1;
}
return res%mod;
}
inline int C(int n,int m){
if(n-m<0) return 0;
return jc[n]*nijc[m]%mod*nijc[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
jc[0]=1,nijc[0]=1;
for(int i=1;i<=2e6;i++) jc[i]=jc[i-1]*i%mod,nijc[i]=qpow(jc[i],mod-2)%mod;
for(int i=1;i<=2e6;i++) sum[i]=sum[i-1]+i;
int tc;cin>>tc;
while(tc--){
cin>>n;
int x=lower_bound(sum+1,sum+pos+1,n)-sum;
int y=(n-sum[x-1]-1)%x+1,ans=0; --x,--y;
for(int k=0;k<=min(x,y);k++){
int tmp=((C(x+y-2*k,x-k)-C(x+y-2*k,x-k+1))%mod+mod)%mod;
ans=(ans+C(x+y-k,k)*tmp%mod)%mod;
}
cout<<ans<<"\n";
}
return 0;
}

浙公网安备 33010602011771号