基础计数

学习笔记

Posted by WrongAnswer_90's Blog on October 10, 2022

My Blogs

开个新坑,目前大多数是蓝书上的题。

不会更高级的东西,只写怎么数数,不考虑高级优化。

状态设计:这里满足的要求不再是无后效性,而是要求一个阶段的所有状态能不重不漏的覆盖掉所有情况。

转移:寻找合适的基准点,围绕这个基准点把大的状态拆出一个小的不可划分的状态,和剩下的状态进行计算(一般是乘起来)。

过河卒plus

基础题。黑色格子很少,但是棋盘大小非常大,做法应当和值域无关,而是和黑格子的数量相关。先把黑色格子按横坐标为第一关键字,纵坐标为第二关键字排序。

引理:从 $(0,0)$ 只向下和向右走到 $(n,m)$ 的方案数为 $\binom{n+m}{n}$。

证明:一定向下走 $n$ 步,向右走 $m$ 步,总步数 $n+m$。看成一个 $n+m$ 的数列,把其中 $n$ 个染成黑色,其余白色,表示黑色的操作是向下走,所以答案即为 $\binom{n+m}{n}$。

直接算不经过所有黑格子的路径数是不好算的。正难则反,考虑用容斥的思想,设计 $f_i$ 表示只经过第 $i$ 个黑色格子并且停留在第 $i$ 个黑色格子的方案数。

转移方程为 $f_i=\binom{x_i+y_i}{x_i}-\sum_{j=1}^{n}[x_j\leq x_i\wedge y_j\leq y_i]\binom{x_i-x_j+y_i-y_j}{x_i-x_j}f_j$。

如果没有任何限制,则 $f_i=\binom{x_i+y_i}{x_i}$。划分基准点:即枚举第一个经过的黑色格子 $j$ 为基准点。基准点前面只经过了白色格子,是不可划分的小状态。基准点后面到 $i$ 的路径是随便走的,是大的状态减去不可划分的状态剩下的剩余状态,这部分没有限制,可以随便走,因为我们的不可划分状态是经过格子 $j$ 前面的路径,这样才能做到不重不漏。

最后 $ans=\binom{A+B}{A}-\sum_{j=1}^n\binom{A+B-x_j-y_j}{A-x_j}f_j$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
	void exgcd(int a,int b,int &x,int &y)
	{
		if(!b)return x=1,y=0,void();
		exgcd(b,a%b,x,y);int z=x;x=y,y=z-x*(a/b);
	}
	int f[2002],x,y,g[200001],inv[200001];
	pii a[2002];
	inline int C(int n,int m){return g[n]*inv[m]%MOD*inv[n-m]%MOD;}
	int n;
	inline void mian()
	{
		g[0]=inv[0]=1;
		for(int i=1;i<=200000;++i)g[i]=g[i-1]*i%MOD,exgcd(g[i],MOD,inv[i],y),inv[i]=(inv[i]%MOD+MOD)%MOD;
		read(x,y,n),g[0]=1,a[0].fi=a[0].se=1,a[n+1].fi=x,a[n+1].se=y;
		for(int i=1;i<=n;++i)read(a[i].fi,a[i].se);
		sort(a+1,a+1+n);
		for(int i=1;i<=n+1;++i)
		{
			f[i]=C(a[i].fi+a[i].se-2,a[i].fi-1);
			for(int j=0;j<i;++j)
			{
				if(a[j].fi<=a[i].fi&&a[j].se<=a[i].se)
				f[i]=(f[i]-f[j]*C(a[i].fi-a[j].fi+a[i].se-a[j].se,a[i].fi-a[j].fi)%MOD+MOD)%MOD;
			}
		}
		write(f[n+1]);
	}

过河卒plusplus

唯一和上题不同在于可以向右下走。设 $d_{i,j}$ 为从 $(0,0)$ 走到 $(i,j)$ 的方案数。

枚举向右下走了几次,可以得到:

\[d_{i,j}=\sum_{k=0}^{\min(i,j)}\binom{i-k+j-k+k}{k}\binom{i-k+j-k}{i-k}\]

第一个组合数表示剩下走 $i-k+j-k$ 中穿插着 $k$ 步,插板法,右边表示正常走。

发现模数很小,值域相对较大,组合数可以 Lucas 定理 $\mathcal O(\log_p n)$ 求。

然后就和上一道题一样了,状态和转移都是类似的。复杂度 $\mathcal O(k^2\min(n,m)\log_p n)$。

连通图

开始厉害了。

还是考虑容斥。$n$ 个点的连通图共有 $\dfrac{n(n-1)}{2}$ 条边,总方案数是 $2^{\frac{n(n-1)}{2}}$,考虑如何计算不合法的方案数。

选取基准点,枚举 $i$ 所在的连通块的大小 $j$,要从剩下的 $i-1$ 个点中选 $j-1$ 个(因为 $1$ 号点必选),剩余的点间随意连边,得到转移方程:

$f_i=2^{\frac{n(n-1)}{2}}-\sum_{j=1}^{i-1}\binom{i-1}{j-1}2^{\frac{(i-j)(i-j-1)}{2}}f_j$。

这里不可划分的状态即为 $i$ 所在的连通块,已经强制保证它联通,所以只需考虑剩下的点和如何选点的问题。要开高精。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
	int n;
	inline void mian()
	{
		C[1][1].len=1,C[1][1].a[1]=1;
		for(int i=2;i<=50;++i)for(int j=1;j<=i;++j)C[i][j]=C[i-1][j]+C[i-1][j-1];
		Node nd1;memset(nd1.a,0,sizeof(nd1.a)),nd1.a[nd1.len=1]=2;
		for(int i=1;i<=50;++i)
		{
			f[i]=power(nd1,(i*(i-1))>>1);
			for(int j=1;j<i;++j)
			f[i]=f[i]-(f[j]*C[i][j]*power(nd1,((i-j)*(i-j-1))>>1));
		}
		while(1)
		{
			read(n);
			if(!n)return;
			print(f[n]);
		}
	}

连通图plus

第一眼设计 $f_{i,j}$ 表示 $i$ 个点,$j$ 条割边的方案数,还是类似上面的方式容斥,总方案数上一个题已经求出,然后减去不合法的状态。然后就不会了。

问题在于无法准确的划分不可分割的子问题。如果还是像上一道题枚举 $1$ 所在边双的大小(设为 $h_i$),但这时删去 $1$ 号点所在边双后可能会分成许许多多的连通块,暴力枚举大小是指数级的,复杂度原地升天。

既然问题在于会出现多个连通块,就考虑加一维状态记录连通块的数量,$g_{i,j,k}$ 表示 $i$ 个点,$j$ 个连通块,$k$ 条割边的方案数。

注意 $g$ 不能代替 $f$。这里可能会觉得 $g_{i,1,j}=f_{i,j}$,但是其实不是,下文会讲。

$g$ 的基准点:首先枚举编号最小的点所在连通块的大小,然后枚举编号最小的点所在联通块内割边的数量:转移方程:

\[g_{i,j,k}=\sum_{p=1}^{i}\sum_{q=0}^{k}f_{p,q}\binom{i-1}{p-1}g_{i-p,j-1,k-q}p\]

$p$ 枚举连通块大小,$q$ 枚举割边数量。组合数是选取连通块的点,$f_{p,q}$ 是该连通块的方案数,$g_{i-p,j-1,k-q}$ 是剩余的方案数。你会发现最后多了一个 $p$,但是正常推式子推不出来。

这里感觉蓝书讲的不是特别清楚,着重写一下。此处其实是一个类似于费用提前计算的思想,因为从 $g$ 转移到 $f$ 的时候对于每个连通块,会乘它的连通块的点数量,表示 $1$ 号点连向了哪个点。所以 $g_{i,j,k}$ 的真实含义是:$i$ 个点,$j$ 个连通块,$k$ 条割边,对后面答案计算的总贡献。这样我们得到了完整的转移方程:

\[\begin{align*} h_i&=2^{\frac{i*(i-1)}{2}}-\sum_{j=1}^{i-1}\binom{i-1}{j-1}2^{\frac{(i-j)*(i-j-1)}{2}}h_j\\ f_{i,j}&=\sum_{k=1}^{i-1}f_{k,0}\binom{i-1}{k-1}\sum_{t=1}^{\min(i-k,j)}k^{t}g_{i-k,t,j-t}\\ f_{i,0}&=h_i-\sum_{j=1}^{i-1}f_{i,j}\\ g_{i,j,k}&=\sum_{p=1}^{i}\sum_{q=0}^{k}f_{p,q}p\binom{i-1}{p-1}g_{i-p,j-1,k-q} \end{align*}\]

第二行的 $k$ 是枚举 $1$ 所在边双的点数,$t$ 枚举移除 $1$ 所在边双后有几个连通块,意义还是很明显的。其中删除后每个连通块的点连向 $1$ 的边的系数上文中已经计算。

初值 $f_{0,0}=g_{0,0,0}=1$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
	int n,m,fr[51],inv[51],f[51][51],g[51][51][51],h[51];
	inline int power(int x,int y){int s=1;for(;y;y>>=1,Mmul(x,x))if(y&1)Mmul(s,x);return s;}
	inline int C(int n,int m){return n<m||m<0?0:Cmul(fr[n],inv[m],inv[n-m]);}
	inline void mian()
	{
		fr[0]=inv[0]=h[0]=1;
		for(int i=1;i<=50;++i)fr[i]=Cmul(fr[i-1],i);
		inv[50]=power(fr[50],MOD-2);
		for(int i=49;i>=1;--i)inv[i]=Cmul(inv[i+1],i+1);
		for(int i=1;i<=50;++i)
		{
			h[i]=power(2,i*(i-1)/2);
			for(int j=1;j<i;++j)Mdel(h[i],Cmul(h[j],C(i-1,j-1),power(2,(i-j)*(i-j-1)/2)));
		}
		g[0][0][0]=f[0][0]=1;
//		for(int i=0;i<=50;++i)for(int j=0;j<=50;++j)g[0][i][j]=1;
		for(int i=1;i<=50;++i)
		{
			f[i][0]=h[i];
			for(int j=1;j<i;++j)
			{
				for(int k=1;k<i;++k)
				{
					for(int t=1;t<=min(i-k,j);++t)
					Madd(f[i][j],Cmul(f[k][0],C(i-1,k-1),power(k,t),g[i-k][t][j-t]));
				}
				Mdel(f[i][0],f[i][j]);
			}
			for(int j=1;j<=i;++j)
			{
				for(int k=0;k+j<=i;++k)
				{
					for(int p=1;p<=i;++p)
					{
						for(int q=0;q<=k;++q)
						Madd(g[i][j][k],Cmul(f[p][q],p,C(i-1,p-1),g[i-p][j-1][k-q]));
					}
				}
			}
		}
		read(n,m);int ans=0;
		for(int j=0;j<=min(n-1,m);++j)Madd(ans,f[n][j]);
		write(ans);
	}