数学 Trick 之:断边+子集反演
能够解决的问题
在树上限制跟边有关的某些问题。
优缺点
优点:思路巧妙,题目有区分度。
缺点:无(有点难理解应该不算吧)。
思路
既然跟边有关,那就对边下手,考虑断开一些边(或者说让这些边一定不具有性质,其他的可以不具有性质),分别求答案,然后子集反演回去。
这个过程可以用 树形 \(\text{dp}\) 实现。
就这么抽象,可以先食用例题。
例题与代码
AT_arc101_c [ARC101E] Ribbons on Tree
他的限制为必须经过每条边,所以我们考虑:让一些边一定不被经过,其他边可以不被经过,求答案,那么就简单了。
- 一个连通块的所有边可以不被经过的方案数即为随机匹配就行:
\[\text{g}(x) =
\begin{cases}
0, x\, \& \, 1 = 1 \\
\prod_{i = 1}^{\frac{x}{2}} (2i - 1), x\, \& \, 1 = 0 \\
\end{cases}
\]
于是转移(\(dp_{u, j}\) 为 \(u\) 的子树,\(u\) 的连通块大小为 \(j\) 这个状态中,除了 \(u\) 的连通块之外的方案数)。
注:这里 \(tmp\) 是转移前 \(dp\) 的备份,因为这里的每个转移相互独立。
\[dp_{u, j} = dp_{u, j} + (-1) \times g(k) \times tmp_{u, j} \times dp_{son, k}
\]
这是将 \((u, son)\) 这条边断掉的转移我们需要实时反演(根据子集反演,断了一条边,需要变号)。
\[dp_{u, j + k} = dp_{u, j + k} + tmp_{u, j} \times dp_{son, k}
\]
不断边就简单了。
我知道这非常非常难理解,但是确实只能这么说了,盯着式子意会吧 · · ·
代码!
#include <bits/stdc++.h>
using namespace std;
constexpr int maxn = 5005, modd = 1000000007;
int n, sz[maxn], g[maxn], head[maxn], tot, dp[maxn][maxn], tmp[maxn], ans;
struct Edge {
int to, nxt;
} e[maxn << 1];
void add(int u, int v) {
e[++tot] = {v, head[u]};
head[u] = tot;
}
void dfs(int u, int fa) {
sz[u] = 1;
dp[u][1] = 1;
for (int i = head[u], now; i; i = e[i].nxt) {
if (now == fa) continue;
dfs(now, u);
for (int j = 1; j <= sz[u]; j++) tmp[j] = dp[u][j], dp[u][j] = 0;
for (int j = 1; j <= sz[u]; j++)
for (int k = 1; k <= sz[now]; k++) {
(dp[u][j] += modd - 1ll * tmp[j] * dp[now][k] % modd * g[k] % modd) %= modd;
(dp[u][j + k] += 1ll * tmp[j] * dp[now][k] % modd) %= modd;
}
sz[u] += sz[now];
}
return ;
}
signed main() {
ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
cin >> n;
for (int i = 1, u, v; i < n; i++) {
cin >> u >> v;
add(u, v);
add(v, u);
}
g[0] = 1;
for (int i = 2; i <= n; i += 2) {
g[i] = 1ll * g[i - 2] * (i - 1) % modd;
}
dfs(1, 0);
for (int i = 1; i <= n; i++) {
(ans += 1ll * dp[1][i] * g[i] % modd) %= modd;
}
cout << ans << '\n';
return 0;
}

浙公网安备 33010602011771号