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;
}