计数DP总结

P3244 [HNOI2015] 落忆枫音

记每个节点的入度为\(in_i\)
易得对于一个DAG,其生成树数量为\(\prod_{i=1}^{n} in_i\)
但加了新边之后,这张图可能会存在环,会出现一些方案是不合法的。我们便要容斥一下,把不合法的方案去掉。
那我们就考虑不合法方案数怎么算。
记新加的边为\(s->t\)
那么环的数量就等同于原图中\(t->s\)路径的数量
那么我们再去考虑每一个环
我们可以给环上每个节点钦定一个父亲
那么环上每个节点对于答案的贡献就从\(in_i\)变为了\(1\)
记环上的节点为\(w\)
那么这个环对于不合法方案数的贡献即为\(\frac{\prod_{i=1}^{n} in_i}{\prod in_w}\)
注意到这个东西可以dp一下
\(f_i\)为从\(i\)\(s\)的路径中上面这个式子的值。
初始化\(f_s\)为原图中\(\prod_{i=1}^{n} in_i \div in_s\)
转移方程:\(f_u = \frac{\sum_{v}^{} f_v}{in_u}(u->v)\)
最终的答案即为\(f_t\)
那我们便可以反向建图,在反图中记忆化搜索,在回溯时进行转移。
注意,因为转移方程中有除法,所以要求逆元。

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e5 + 7, M = 2e5 + 7, p = 1e9 + 7;
int n, m, from, to;
ll dp[N];
ll ans, du[N], f[N], s;
bool vis[N];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % p;
		x = x * x % p;
		y >>= 1;
	}
	return sum;
}
struct Edge{
	int head[N], tot;
	struct edge{
		int to, pre;
	}e[M];
	void add(int x, int y){
		e[++tot] = {y, head[x]};   
		head[x] = tot;
		du[x]++;
	}
	void dfs(int u){
		if(vis[u] || u == to) return;
		vis[u] = 1;
		for(int i = head[u]; i; i = e[i].pre){
			int v = e[i].to;
			dfs(v);
			f[u] = (f[u] + f[v] * fpow(du[u], p - 2) % p) % p;
		}
	}
}E;
int main(){
	scanf("%d%d%d%d", &n, &m, &from, &to);
	for(int i = 1, x, y; i <= m; i++){
		scanf("%d%d", &x, &y);
		E.add(y, x);
	}
	ans = s = du[1] = 1;
	for(int i = 1; i <= n; i++){
		if(i == to) ans = ans * (du[i] + 1) % p;
		else ans = ans * du[i] % p;
		s = s * du[i] % p;
	}
	f[to] = (s * fpow(du[to], p - 2)) % p;
	E.dfs(from);
	ans = (ans - f[from] + p) % p;
	if(to == 1 || from == to) printf("%lld\n", s);
	else printf("%lld\n", ans);
	return 0;
}

P3214 [HNOI2011] 卡农

将题意转化一下:
从集合\(S={1,2,3,\dots}\)中选取\(m\)个子集,要求
(1):不存在空集
(2):不能有重复的子集
(3):在这\(m\)个子集中,\(1\)\(n\)每个元素的出现次数都必须是偶数
\(f_i\)为选取\(i\)个子集的方案数
试着用容斥去做
根据第三条性质,可得当前\(i-1\)个集合都选取后,第\(i\)个集合也随之确定,总方案数为\(A_{2^n-1}^{i-1}\)
根据第一条性质,可得当第\(i\)个集合为空集时,前\(i-1\)个集合是合法的,那么不符合第一条性质的不合法方案数即为\(f_{i-1}\)
根据第二条性质,当第\(i\)个集合与第\(j\)个集合重复时,另外的\(i-2\)个集合是合法的,方案数为\(f_{i-2}\),又因为\(j\)\(i-1\)个取值,子集\(i\)\(2^n-1-(i-2)\)个取值,那么不符合第二条性质的不合法方案数即为\(f_{i-2} \times (i-1) \times (2^n-i+1)\)
综上,转移方程即为\(f_i=A_{2^n-1}^{i-1}-f_{i-1}-f_{i-2} \times (i-1) \times (2^n-i+1)\)

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 7, mod = 1e8 + 7;
int n, m; 
ll a[N], f[N];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return sum;
}
int main(){
	scanf("%d%d", &n, &m);
	a[0] = f[0] = 1;
	ll t = fpow(2, n) - 1, r = 1;
	for(int i = 1; i <= m; i++){
		a[i] = a[i - 1] * (t - i + 1 + mod) % mod;
		r = r * i % mod;
		//printf("a%d:%lld\n", i, a[i]);
	}
	for(int i = 1; i <= m; i++){
		f[i] = (a[i - 1] - f[i - 1] + mod - f[i - 2] * (i - 1) % mod * (t - i + 2) % mod + mod) % mod;
	}
	printf("%lld\n", f[m] * fpow(r, mod - 2) % mod);
	return 0;
}

P2606 [ZJOI2010] 排列计数

我们可以将题意转换成一个满足小根堆性质的完全二叉树的树的个数
然后就很好处理了
\(f_i\)\(i\)个节点的树的完全二叉树的个数
先算出左右子树的节点个数\(l,r\)
则可推出\(f_i=C^{i-1}_{l} \times f_l \times f_r\)
注意:组合数要用lucas定理算

Code:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6;
#define ll long long
int Task;
int n, p;
int lg2[N + 7];
ll fac[N + 7], inv[N + 7];
ll f[N + 7];
ll fpow(ll x, ll y){
	ll sum = 1;
	while(y){
		if(y & 1) sum = sum * x % p;
		x = x * x % p;
		y >>= 1;
	}
	return sum;
}
void init(){
	f[1] = fac[0] = fac[1] = 1;
	for(int i = 2; i <= N; i++){
		lg2[i] = lg2[i >> 1] + 1;
		fac[i] = fac[i - 1] * i % p;
	}
	int m = min(p - 1, n);
	inv[m] = fpow(fac[m], p - 2);
	for(int i = m - 1; i >= 0; i--){
		inv[i] = inv[i + 1] * (i + 1) % p;
	}
}
ll C(ll x, ll k){
	return fac[x] * inv[k] % p * inv[x - k] % p;
}
ll lucas(ll x, ll k){
	return k == 0 ? 1 : (C(x % p, k % p) * lucas(x / p, k / p) % p);
}
int main(){
	scanf("%d%d", &n, &p);
	init();
	f[1] = f[2] = 1;
	f[3] = 2;
	int l = 1, r = 1;
	for(int i = 4; i <= n; i++){
		if(i - (1 << lg2[i]) + 1 <= (1 << (lg2[i] - 1))) l++;
		else r++;
		f[i] = lucas(i - 1, l) * f[l] % p * f[r] % p;
	}
	printf("%lld\n", f[n]);
	return 0;
}

P6846 [CEOI 2019] Amusement Park

可以发现对于每一种合法的方案
这个方案的反图也必定合法
且这两个方案对于答案的贡献之和为\(m\)
那么就可以将问题转化为求合法方案数再乘上\(\frac{m}{2}\)
随后开始想怎么求合法方案数
可以想到对于一个\(DAG\),可以将其分解为一个入度为0的点集和一个更小的\(DAG\)
再结合这道题\(n\leq18\)的数据范围
就可以以这个点集为依据去\(dp\)
这也是DAG上DP的一个常用套路:钦定零度点集,简单容斥去重
\(T\)\(S\)的一个子集
\(T\)为独立集
\(f_S=\sum f_{S-T}\)
但这样算会算重
举一个很简单的例子
若原图为{\({3 \leftarrow 1, 3 \leftarrow 2}\)}
那么在枚举\(1\)\(2\)\(1,2\)这三个点集时
{\({3 \rightarrow 1, 3 \rightarrow 2}\)}这个方案会被重复统计
毕竟这个方案对于三个集合都满足集合内点的入度为0
那么我们便要容斥一下
\(T\)\(S\)中最大的零度点集
因为\(T\)合法
那么\(T\)的所有子集也必定合法
这便是一个老生常谈的容斥:当\(|T|\)为奇数时,令其贡献为正,否则令其贡献为负
那么就可以AC这道题了

Code:

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 18, M = 153, mod = 998244353, inv = (mod + 1) >> 1;
int n, m;
int num[(1 << N) + 7];
ll f[(1 << N) + 7];
bool is[(1 << N) + 7];
struct Edge{
	int x, y;
}e[M];
int main(){
	scanf("%d%d", &n, &m);
	for(int i = 1; i <= m; i++){
		scanf("%d%d", &e[i].x, &e[i].y);
		e[i].x--;
		e[i].y--;
	}
	for(int S = 0; S < (1 << n); S++){
		is[S] = 1;
		for(int i = 0; i < n; i++){
			num[S] += ((S >> i) & 1);
		}
		for(int i = 1; i <= m; i++){
			if((S & (1 << e[i].x)) && (S & (1 << e[i].y))){
				is[S] = 0;
				break;
			}
		}
	}
	f[0] = 1;
	for(int S = 1; S < (1 << n); S++){
		for(int T = S; T; T = (T - 1) & S){
			if(!is[T]) continue;
			if(num[T] & 1){
				f[S] = (f[S] + f[S ^ T]) % mod;
			}
			else{
				f[S] = (f[S] - f[S ^ T] + mod) % mod;
			}
		}
	}
	printf("%lld\n", f[(1 << n) - 1] * m % mod * inv % mod);
	return 0;
}
posted @ 2026-01-22 13:39  EzSun599  阅读(3)  评论(0)    收藏  举报