DP mix 容斥

简介

在一类题中,我们需要用 dp 求答案,最后再熔池算出答案,这样复杂度与 dp 有关。

但是我们也可以将容斥系数直接套进 dp 里,这样可以减少一维状态。

例题

P4099 [HEOI2013] SAO

题意: 一棵树,但是边有方向,求拓扑序方案数。

思路:

如果这棵树是内向树或外向树,显然我们可以用 dp 求解。

所以我们考虑计算外向树,然后把所有不符合条件的边容斥掉。不妨设 \(f_{i,j,k}\) 表示 \(i\) 的子树内 \(j\) 条边由不符合反转成符合,\(i\) 所在连通块大小为 \(k\)

显然是 \(O(n^3)\),我们发现最后的答案是 \(\sum (-1)^jf_{rt, j, k}\),所以我们可以考虑将 \(j\) 这一维代入 dp 值里。

\(g_{i,k} = \sum (-1)^j f_{i,j,k}\),则我们考虑对 \(g\) 进行 dp。

我们可以先把 \(f\) 的转移写出来:

如果这条边符合:

\[\binom{k + k' - 1}{k'}f(x,j,k)f(y,j',k') \to F(x,j + j', k + k') \]

如果不符合:

\[\binom{k + k' - 1}{k'}f(x,j,k)f(y,j',k') \to F(x,j + j' + 1, k + k') \]

\[\frac{1}{k'!}f(x,j,k)f(y,j',k') \to F(x,j + j', k) \]

发现这就是类似卷积的形式,于是我们可以将 \(g\) 的转移写出来:

\[\binom{k + k' - 1}{k'}g(x,k)g(y,k') \to G(x,k + k') \]

\[(-1)\binom{k + k' - 1}{k'}g(x,k)g(y,k') \to G(x,k + k') \]

\[\frac{1}{k!}g(x,k)g(y,k') \to G(x,k) \]

然后我们就做完了。

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
const int mod = 1e9 + 7;

int fpow(int a, int b, int p) {
	if (b == 0)
		return 1;
	int ans = fpow(a, b / 2, p);
	ans = 1ll * ans * ans % p;
	if (b % 2 == 1)
		ans = 1ll * a * ans % p;
	return ans;
}

int fac[N] = {0}, inv[N] = {0};
int cmb[N][N] = {{0}};

void init(int n) {
	fac[0] = 1;
	for (int i = 1; i <= n; i++)
		fac[i] = 1ll * fac[i - 1] * i % mod;
	inv[n] = fpow(fac[n], mod - 2, mod);
	for (int i = n - 1; i >= 0; i--)
		inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
	cmb[0][0] = 1;
	for (int i = 1; i <= n; i++) {
		cmb[i][0] = 1;
		for (int j = 1; j <= i; j++)
			cmb[i][j] = (cmb[i - 1][j] + cmb[i - 1][j - 1]) % mod;
	}
}

int n;
struct Edge {
	int to, val;
	Edge (int _to = 0, int _val = 0) :
		to(_to), val(_val) {}
}; 
vector<Edge> e[N];

int f[N][N] = {{0}};
int g[N][N] = {{0}};
int sz[N] = {0};

void add(int &x, int y) {
	x = (x + y) % mod;
} 

int srh(int x, int pr) {
	for (auto i: e[x])
		if (i.to != pr)	
			srh(i.to, x);
	//初始化
	f[x][1] = sz[x] = 1;
	//转移
	for (auto i: e[x]) {
		int v = i.to, w = i.val;
		if (v == pr)
			continue;
        for (int j = 1; j <= sz[x] + sz[v]; j++)	
			g[x][j] = 0;
		if (w == 1) {
			for (int j = 1; j <= sz[x]; j++)
				for (int k = 1; k <= sz[v]; k++)
					add(g[x][j + k], 1ll * f[x][j] * f[v][k] % mod * cmb[j + k - 1][k] % mod);
		}
		else {
			for (int j = 1; j <= sz[x]; j++)
				for (int k = 1; k <= sz[v]; k++) {
					add(g[x][j + k], 1ll * (mod - 1) * f[x][j] % mod * f[v][k] % mod * cmb[j + k - 1][k] % mod);
					add(g[x][j], 1ll * f[x][j] * f[v][k] % mod * inv[k] % mod);
				}
		} 
		sz[x] += sz[v];
		for (int j = 1; j <= sz[x]; j++)
			f[x][j] = g[x][j];
	//	cout << "after " << x << " " << v << endl;
	//	for (int j = 1; j <= sz[x]; j++)
   //	     	cout << x << " " << j << " " << f[x][j] << endl;	
	} 
    
	return sz[x];
}

void slv() {
	cin >> n;
	init(n);
	char w;
	for (int i = 1; i <= n; i++)
		e[i].clear();
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> w >> v;
		u++, v++;
		e[u].push_back(Edge(v, (w == '<')));
		e[v].push_back(Edge(u, (w == '>')));
	}
	for (int i = 0; i <= n; i++)
		for (int j = 0; j <= n; j++)
			f[i][j] = 0;
	srh(1, 0);
	int ans = 0;
	for (int k = 1; k <= n; k++)
		add(ans, 1ll * fac[n] * inv[k] % mod * f[1][k] % mod);
	cout << ans << endl;
}

int main() {
	int T;
	cin >> T;
	while (T--)
		slv();
	return 0;
} 
[ARC101E] Ribbons on Tree

题意: 一棵树,求多少种方案将顶点两两配对,使得将每个点对的路径上的所有边全部系上丝带后满足,每条边都有至少一条丝带。

思路:

\(n\) 个点两两配对的方案数是所有小于 \(n\) 的奇数的乘积。这个很容易就可以推出来。

如果我们钦定哪些边不覆盖,方案数很好计算,所以我们考虑容斥,\(F(i,j,k)\) 表示 \(i\) 子树内,有 \(j\) 条边未覆盖,其他任意,当前与 \(i\) 联通的点有 \(k\) 个,求方案数。

接着就是和上道题几乎一样的容斥与转移。

点击查看代码
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 5005;
const int mod = 1e9 + 7;

int fpow(int a, int b, int p) {
	if (b == 0)
		return 1;
	int ans = fpow(a, b / 2, p);
	ans = 1ll * ans * ans % p;
	if (b % 2 == 1)
		ans = 1ll * a * ans % p;
	return ans; 
}

int n;
vector<int> e[N];
int f[N][N] = {{0}}, g[N][N] = {{0}};

int h[N] = {0}, inv[N] = {0};

void add(int &x, int y) {
	x = (x + y) % mod;
}

void init() {
	h[1] = 1, h[2] = 1;
	for (int i = 3; i <= n; i++)
		h[i] = (i % 2 == 1 ? 1 : 1ll * (i - 1) * h[i - 2] % mod);
	for (int i = 1; i <= n; i++)
		inv[i] = fpow(h[i], mod - 2, mod);
} 

int sz[N] = {0};

void srh(int x, int pr) {
	for (auto i: e[x])
		if (i != pr)
			srh(i, x);
	sz[x] = f[x][1] = 1;
	for (auto i: e[x])
		if (i != pr) {
			for (int j = 1; j <= sz[x]; j++)
				g[x][j] = 0;
			for (int j = 1; j <= sz[x]; j++)
				for (int k = 1; k <= sz[i]; k++) {
					if (k % 2 == 0)
						add(g[x][j], 1ll * f[x][j] * f[i][k] % mod * (mod - 1) % mod);
					add(g[x][j + k], 1ll * f[x][j] * f[i][k] % mod * h[j + k] % mod * inv[j] % mod * inv[k] % mod);
				}
			sz[x] += sz[i];
			for (int j = 1; j <= sz[x]; j++)
				f[x][j] = g[x][j];
		}		
}

int main() {
	cin >> n;
	for (int i = 1, u, v; i < n; i++) {
		cin >> u >> v;
		e[u].push_back(v);
		e[v].push_back(u);
	}	
	init();
	srh(1, 0);
	int ans = 0;
	for (int i = 2; i <= n; i += 2)
		add(ans, f[1][i]);
	cout << ans << endl;
	return 0;
}
posted @ 2024-03-21 19:02  rlc202204  阅读(18)  评论(0编辑  收藏  举报