卡特兰数与反射容斥
双射之美,古来共谈
卡特兰数
还是比较常用的。常见用以表示:
- 求长度为\(2n\)的合法出栈序列。
- 求长度为\(2n\)的合法括号序列。
- 求从\((0,0)\)到\((n,n)\),只能向右或向上走,其中不跨过\(y=x\)这条直线的方案数。
- ......
直接上式子,这里给出两种公式:
反射容斥·单线
将从该角度给出一种卡特兰数公式的证明。
考虑求从\((0,0)\)到\((n,n)\),只能向右或向上走(本文以下提到的都为这种走法),其中不跨过\(y=x\)这条直线的方案数。如果没有限制随便走,方案数是\(C_{2n}^n\)的。尝试转化限制。不跨过\(y=x\)等价与不碰到\(y=x+1\)。注意这两者的区别。
想一想碰到\(y=x+1\)的方案数怎么算。显然,对于一条路线,如果从第一次碰到\(y=x+1\)开始,后面的路线全都以\(y=x+1\)为轴对称翻上去,将唯一对应一条走向\((n-1,n+1)\)的路线。因此方案数为\(C_{2n}^{n-1}\)。
图册P2给出了一个示意图。
这里证明了上面第一个卡特兰数公式。
更加一般的,从\((0,0)\)到\((n,m)\),只能向右或向上走,其中不跨过\(y=x+b\)这条直线的方案数为\(C_{n+m}^n-C_{n+m}^{m-b}\)。
反射容斥·双线
考虑求从\((0,0)\)到\((n,m)\),只能向右或向上走(本文以下提到的都为这种走法),其中向上不碰到\(y_1=x+k_1\),向下不碰到\(y_2=x+k_2(k_2<0)\)的方案数。为了方便表述,我们用\(A\)和\(B\)来表述一种行走的序列,其中\(A\)表示碰到了\(y_1\),\(B\)表示碰到了\(y_2\)。不合法方案可以简单的分为两种:以\(A\)开头或者以\(B\)开头。先来考虑以\(A\)开头的方案,以\(B\)开头是与之类似的。仿照单线的思路,将点\((n,m)\)以\(y_1\)为轴对称到\((m-k_1,n+k_1)\),求一次\((0,0)\)到它的方案数。
可以发现,我们并不能保证每条路线都是以\(A\)开头,事实上我们只求得了形如\(....A....\)的方案。既然这样那就减去形如\(..B.A....\)的方案。诶不是,那好像又多减了\(.A.B.A....\)的方案。得再把它加上。我们这样一直容斥下去,直到\(A\)前面不会再出现\(B\),那么以\(A\)开头的方案就算完了。这个如何实现,还是仿照单线容斥的思路去考虑,以\(y_1\)为轴对称一次,路线序列将一定包含\(A\),将对称后的点再以\(y_2\)为轴对称一次,将一定包含\(BA\),再将新得到的点以\(y_1\)为轴对称一次,将得到\(ABA\)......这个东西画一画图就能得到。于是我们只需要一直对称下去,直到新点不在第一象限,这时就不再产生贡献。
以\(B\)开头是类似做的。
复杂度为\(O(\cfrac{n+m}{|n-m|})\)。
无论单线双线,正确性都基于双射。
应用
这个东西有什么用呢?
对于一些DP,它可能长得像有限制的格路计数,可以往反射容斥上转来优化时间复杂度。
例题为「JLOI2015」骗我呢。
点击查看代码
int n, m;
ll jc[N], inv[N];
ll quick(ll x, ll y){
ll rs = 1ll;
while(y){
if(y & 1) (rs *= x) %= mod;
(x *= x) %= mod;
y >>= 1;
}
return rs;
}
void fold1(int &x, int &y){
swap(x, y);
x--, y++;
}
void fold2(int &x, int &y){
swap(x, y);
x += m + 2, y -= m + 2;
}
ll C(ll x, ll y){
if(x < y) return 0;
if(x < 0 || y < 0) return 0;
return jc[x] * inv[y] % mod * inv[x - y] % mod;
}
signed main(){
n = read(), m = read();
jc[0] = inv[0] = 1ll;
for(int i = 1; i <= N - 5; i++) jc[i] = jc[i - 1] * i % mod;
inv[N - 5] = quick(jc[N - 5], mod - 2);
for(int i = N - 6; i; i--) inv[i] = inv[i + 1] * (i + 1) % mod;
int x = n + m + 1, y = n;
ll res = C(n + m + 1 + n, n);
while(x >= 0 && y >= 0){
fold1(x, y);
res -= C(x + y, y);
(res += mod) %= mod;
fold2(x, y);
res += C(x + y, y);
res %= mod;
}
x = n + m + 1, y = n;
while(x >= 0 && y >= 0){
fold2(x, y);
res -= C(x + y, y);
(res += mod) %= mod;
fold1(x, y);
res += C(x + y, y);
res %= mod;
}
cout << res;
return 0;
}
可能需要拉直转移,旋转坐标系的trick,挺烦人的。前提是要先想到暴力DP并适当转化。

浙公网安备 33010602011771号