一类计数类DP的解法

文章讲述了如何解决两个编程问题:在大的棋盘上计算从左上角到左下角不经过黑色格子的路径数,以及求解特定条件下的无向连通图数量。通过动态规划的方法,分别给出了计算方案数的公式和状态转移方程,并提供了相应的代码实现。
摘要由CSDN通过智能技术生成

Gerald ans Giant Chess(P334)

题意:

给定一个 n ∗ m , n , m ≤ 1 e 5 n*m,n,m\leq 1e5 nm,n,m1e5的棋盘,其中有 N ≤ 2000 N\leq 2000 N2000个格子是黑色的,其它都是白色的,求从左上角走到左下角且不能经过黑色格子的方案数,每次选择向右或下走。

分析:

观察到 n , m n,m n,m很大但 N N N很小,考虑从黑色格子入手,从左上角走到右下角的方案数显然是
C ( n + m − 2 , n − 1 ) C(n+m-2,n-1) C(n+m2,n1)。我们考虑容斥,去减掉经过了至少一个黑色格子的路线,考虑如何设置状态做到不重不漏。我们将所有黑色格子先排序,那么我们去固定我们第一个经过的黑色格子的位置,那么所有的子问题将不会有重叠。定义 f [ i ] f[i] f[i]为从左上角走到第 i i i个黑色格子,并且途中不经过其它黑色格子的数量。转移很简单:
f [ i ] = C ( x i + y i − 2 , x i − 1 ) − ∑ j = 0 i − 1 f [ j ] ∗ C ( x i − x j + y i − y j , x i − x j ) f[i]=C(x_i+y_i-2,x_i-1) - \sum_{j=0}^{i-1}{f[j]*C(x_i-x_j+y_i-y_j,x_i-x_j)} f[i]=C(xi+yi2,xi1)j=0i1f[j]C(xixj+yiyj,xixj)
我们把右下角的格子也当作黑色,那么答案即 f [ N + 1 ] f[N+1] f[N+1]

Connected Graph

题意:

N ≤ 50 N\leq 50 N50个节点的无向连通图有多少个,节点有编号。

分析:

显然 N N N个节点的无向图有 2 N ∗ ( N − 1 ) / 2 2^{N*(N-1)/2} 2N(N1)/2个,那么我们考虑计算出不连通的图的个数。
考虑如何设计状态不会重复。
既然是不连通的图,那么必然是若干个连通块形成的,那么我们去枚举节点1所在连通块的大小即可不会造成重复。我们定义 f [ i ] f[i] f[i] i i i个点构成的无向联通图的个数。
转移的话我们去枚举节点1所在联通块的大小为 j j j,那么我们可以在剩下 i − j i-j ij个节点选 j − 1 j-1 j1个与1构成连通块,其它节点随意。
f [ i ] = 2 i ∗ ( i − 1 ) / 2 − ∑ j = 1 i − 1 f [ j ] ∗ C ( i − 1 , j − 1 ) ∗ 2 ( i − j ) ∗ ( i − j − 1 ) / 2 f[i]=2^{i*(i-1)/2}-\sum_{j=1}^{i-1}{f[j]*C(i-1,j-1)*2^{(i-j)*(i-j-1)/2}} f[i]=2i(i1)/2j=1i1f[j]C(i1,j1)2(ij)(ij1)/2

How Many of Them?

题意:

求含有 N N N个节点构成的无向连通图中,割边恰好有 M M M条的方案数,节点编号 1 − N 1-N 1N

分析:

我们称不含割边的极大联通子图为双连通分量。
首先定义 F [ i ] [ j ] : F[i][j]: F[i][j]: i i i个点构成的包含 j j j条割边的无向图的数量。
那么我们仿照之前的做法,枚举1所在的双连通分量的大小 k k k,方案数为 F [ k , 0 ] ∗ C ( i − k , k − 1 ) F[k,0]*C(i-k,k-1) F[k,0]C(ik,k1)
如果把图中的双连通分量都看成点的话,那么最终形成一颗树,树边连接各个双连通分量。那么我们将节点1所在联通分量从图中删去,那么图将会拆分成若干个联通块。
我们令 G [ i , j , k ] G[i,j,k] G[i,j,k]表示由 i i i个点构成的,包含 j j j个联通块和 k k k条割边的无向图数量,同时从每个联通块中选出1个点与节点1所在连通块连接。
那么我们枚举将1所在双连通分量删去后图中连通块的数量 x x x,因为每一个连通块之前都与节点1所在连通块通过割边相连,所有已经消耗 x x x条割边了。同时,因为节点1的双连通分量有 k k k个节点,所以每个连通块都可以选择任意一个节点相连,所以有如下转移:
F [ i ] [ j ] = ∑ k = 1 i − 1 ( F [ k , 0 ] ∗ C ( i − 1 , k − 1 ) ∗ ∑ x = 1 m i n ( i − k , j ) G [ i − k , x , j − x ] ∗ k x ) , j > 0 F[i][j]=\sum_{k=1}^{i-1}{(F[k,0]*C(i-1,k-1)*\sum_{x=1}^{min(i-k,j)}{G[i-k,x,j-x]*k^x})},j>0 F[i][j]=k=1i1(F[k,0]C(i1,k1)x=1min(ik,j)G[ik,x,jx]kx)j>0
特别的:定义 H [ i ] H[i] H[i] i i i个节点构成的无向连通图的数量,在上题中我们已经求得。
那么: F [ i ] [ j ] = H [ i ] − ∑ j = 1 i − 1 F [ i ] [ j ] F[i][j]=H[i]-\sum_{j=1}^{i-1}{F[i][j]} F[i][j]=H[i]j=1i1F[i][j]
最后考虑 G [ i ] [ j ] [ k ] G[i][j][k] G[i][j][k]的转移:我们以编号最小的节点所在连通块为基准,枚举连通块大小 p p p,割边数量 q q q。则:
G [ i ] [ j ] [ k ] = ∑ p = 1 i ∑ q = 0 k F [ p , q ] ∗ C ( i − 1 , p − 1 ) ∗ p ∗ G [ i − p , j − 1 , k − q ] G[i][j][k]=\sum_{p=1}^{i}\sum_{q=0}^{k}F[p,q]*C(i-1,p-1)*p*G[i-p,j-1,k-q] G[i][j][k]=p=1iq=0kF[p,q]C(i1,p1)pG[ip,j1,kq]

code:

mint H[55], f[55][55], g[55][55][55];
bool vis_f[55][55], vis_g[55][55][55];

mint get_f(int i, int j);
mint get_g(int i, int j, int k) {
	if(j > i || k >= i) return 0;
	if(vis_g[i][j][k]) return g[i][j][k];
	g[i][j][k] = 0;
	vis_g[i][j][k] = 1;
	if(j == 1) return g[i][j][k] = get_f(i, k) * i;
	for(int p = 1; p <= i; p++) {
		for(int q = 0; q <= k; q++)
			g[i][j][k] += get_f(p, q) * C(i - 1, p - 1) * p * get_g(i - p, j - 1, k - q);
	}
	return g[i][j][k];
}
mint get_f(int i, int j) {
	if(j >= i) return 0;
	if(vis_f[i][j]) return f[i][j];
	f[i][j] = 0;
	vis_f[i][j] = 1;
	if(j == 0) {
		f[i][j] = H[i];
		for(int k = 1; k <= i - 1; k++) 
			f[i][j] -= get_f(i, k);
		return f[i][j];
	}
	for(int k = 1; k < i; k++) {
		mint s = 0;
		for(int x = 1; x <= min(i - k, j); x++)
			s += get_g(i - k, x, j - x) * mint(k).pow(x);
		s = get_f(k, 0) * C(i - 1, k - 1) * s;
		f[i][j] += s;
	}
	return f[i][j];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr); 
    cout.tie(nullptr);

    init();
   	H[1] = 1;
   	for(int i = 2; i <= 50; i++) {
   		H[i] = mint(2).pow(i * (i - 1) / 2);
   		for(int j = 1; j <= i - 1; j++)
   			H[i] -= H[j] * C(i - 1, j - 1) * mint(2).pow((i - j) * (i - j - 1) / 2);
   	}
   	for(int i = 0; i <= 50; i++)
   		for(int j = 0; j <= 50; j++)
   			f[i][j] = 0;
   	for(int i = 0; i <= 50; i++)
   		for(int j = 0; j <= 50; j++)
   			for(int k = 0; k <= 50; k++)
   				g[i][j][k] = 0;
   	f[1][0] = g[1][1][0] = 1;
   	vis_f[1][0] = vis_g[1][1][0] = 1;

   	int n, m;
   	cin >> n >> m;

   	mint ans = 0;
   	for(int i = 0; i <= m; i++) {
   		ans += get_f(n, i);
   		// cout << i << ' ' << get_f(n, i) << '\n';
   	}
   	cout << ans << '\n';

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值