【ZJOI 2016 小星星】【容斥 + 树形 DP】
来源:互联网 发布:缓存服务器软件 编辑:程序博客网 时间:2024/06/04 18:32
容斥好题喵呜~
我们一共要满足两个限制:
- 树中的一个点对应图中一个点,且一一对应。
- 树中两点有边的,图中两点也对应有边。
–
优化一些的暴力是用
第一次
第二次
枚举当前点
形如:
复杂度 如果你有高深的卡常技巧好像也是可以过的。
如果我们放宽限制,只统计满足限制 2
的方案数,那么每个点映射的点构成的集合是可重集 DP
记
但是我们无法满足限制 1
一一对应,那实际上不合法的状态可以被用两种方法表示:至少有一个点匹配了多个点;至少有一个点没有被匹配到。
这样的话就可以发现强制某些点不选就可以构造出需要去掉的不合法情况,方案数 = 所有点都可以匹配到 - 至少有1个点未被选 + 至少有2个点未被选 - 至少有3个点未被选 …(这里的图上的点可以被树上的点重复覆盖) ,即若至少有偶数个点为被选,就加上,否则减去。
那么对于
我们将这个模型抽象出来:
设
A(i) 表示包含了原图上点i 的映射集合,则答案集合为A(1) 、A(2) 、A(3) …A(n) 的交集,因为|A(1)∩A(2)∩…∩A(n)|=|A(1)∪A(2)∪…∪A(n)|−|A(2)∪…∪A(n)|+… ,所以我们枚举2n−1 个并集,用树上DP
计算出并集的大小,容斥一下就能得到|A(1)∩A(2)∩……∩A(n)| 了。
—— By wzj 大爷
总复杂度
–
复习一下:枚举子集的二进制写法
// 枚举 i 的子集for (int j = i & (i - 1); j; j = i & (j - 1))
这样做就是每次不断 - 1
来枚举所有子集,它不是忽略了 i
中的 0
,而是在 & i
的过程中将 0
消去了。
理解了这个枚举子集抽离点的部分就很容易了。
#include <bits/stdc++.h>#define ll long longusing namespace std;const int N = 25;struct Edge { int next, to; }e[N << 1];ll tot = 0, sum = 0, ans = 0;ll a[N], mapp[N][N], f[N][N];int cnt = 0, head[N];void add(int u, int v) { e[++ cnt].to = v; e[cnt].next = head[u]; head[u] = cnt;}void dfs(int u, int fa) { for (int i = head[u]; i; i = e[i].next) { int v = e[i].to; if (v == fa) continue; dfs(v, u); } for (int i = 1; i <= tot; i ++) { f[u][i] = 1; for (int j = head[u]; j; j = e[j].next) { int v = e[j].to; if (v == fa) continue; ll tmp = 0; for (int k = 1; k <= tot; k ++) if (mapp[a[i]][a[k]]) tmp += f[v][k]; f[u][i] *= tmp; } }}int main() { int n, m; scanf("%d%d", &n, &m); for (int i = 1; i <= m; i ++) { int u, v; scanf("%d%d", &u, &v); mapp[u][v] = 1, mapp[v][u] = 1; } for (int i = 1; i < n; i ++) { int u, v; scanf("%d%d", &u, &v); add(u, v), add(v, u); } for (int i = 1; i <= (1 << n) - 1; i ++) { tot = 0, sum = 0; for (int j = 1; j <= n; j ++) if (i & (1 << (j - 1))) a[++ tot] = j; // a[] 中存的就是枚举出来的状态 i 所包含的点 dfs(1, 0); for (int j = 1; j <= tot; j ++) sum += f[1][j]; ans += (ll)((n - tot) & 1) ? -sum : sum; // 容斥 } printf("%lld\n", ans); return 0; }