卡特兰数与反射容斥

双射之美,古来共谈

卡特兰数

还是比较常用的。常见用以表示:

  • 求长度为\(2n\)的合法出栈序列。
  • 求长度为\(2n\)的合法括号序列。
  • 求从\((0,0)\)\((n,n)\),只能向右或向上走,其中不跨过\(y=x\)这条直线的方案数。
  • ......

直接上式子,这里给出两种公式:

\[H(n)=C_{2n}^{n}-C_{2n}^{n-1}\\ H(n)=H(n-1)\times \cfrac{4n-2}{n+1} \]

反射容斥·单线

将从该角度给出一种卡特兰数公式的证明。
考虑求从\((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并适当转化。

posted @ 2025-09-29 15:25  Lordreamland  阅读(35)  评论(0)    收藏  举报