[dp 记录]P3349 [ZJOI2016]小星星
绝世容斥好题,刚好 NOIp 前要复习容斥,就拉过来当 100 紫了。
祝自己明天的 NOIp rp++
这题好久前看过题解,感觉好可惜,浪费了好题。以后自己不会的题也不能看题解了。
题意:
给定一张图和一棵树,问有几种编号间的一一映射,使得树的结点的编号做此映射后每条树边都包含于图中。
\(n \leq 17\)
\(n\) 很小,所以显然是状压。在图上搞不如在树上搞,因为树上的继承方向是确定的。
思考如何从子树推至父亲。因为要求节点的映射是全集,子树内的点映射到的数字的集合应当被记下来。还要知道从儿子到父亲的边存不存在,因此还要把根节点映射到的编号记下来。由此能写出 \(dp_{i,j,S}\),转移时要使用子集枚举,复杂度上限随便估一下能到 \(O(n^3 3^n)\)。这不能过,因为题解区的人都写过暴力了。
复杂度瓶颈事实上在子集枚举。思考需要子集枚举是因为需要满足节点的映射全集。需要枚举子集的题一般可以试试钦定后找容斥系数,这样可以把“恰好”这个条件反演掉,变成好控制的条件。那么回到这道题,应该反演掉的是节点映射到的点集。“恰好”不好弄,那就弄成包含于或包含。显然这里只有包含于可做。
那么写出 \(dp_{i,j,S}\) 代表根节点为 \(i\),\(i\) 映射到 \(j\),只允许使用点集 \(S\) 中的点。容易发现容斥系数是 \((-1)^{|S|}\)。
这时的转移等均平凡。
初始化时把所有含有 \(i\) 的 \(dp_{u,i,S}\) 设为 \(1\)。
然后就做完了。很自然,感觉。复杂度估一下是 \(O(n^3 2^n)\),能过。
拜谢 第二篇题解,为什么要容斥说的很清楚,也顺带把我之前对容斥系数来源不同之处解释清楚了。
注:关于反演和容斥:我不大区分这两者,容斥应该属于反演,只要能找到容斥 / 反演系数就够了。
#include <cstdio>
#include <cstring>
#define LL long long
using namespace std;
const int M = 18;
struct edge {
int to, nxt;
} e[M*M];
int head[M], cnt1;
void link(int u, int v) {
e[++cnt1] = {v, head[u]}; head[u] = cnt1;
}
int n, m;
bool eg[M][M];
LL dp[M][M]; int lg[1 << M];
void dfs(int u, int fa, int S) {
for(int i = 0; i < n; i++)
if((S >> i) & 1) dp[u][i] = 1;
else dp[u][i] = 0;
for(int x = head[u]; x; x = e[x].nxt) {
int v = e[x].to; if(v == fa) continue;
dfs(v, u, S);
for(int i = S; i; i = i & (i-1)) {
int a = lg[i & -i]; LL tmp = 0;
for(int j = S; j; j = j & (j-1)) {
int b = lg[j & -j];
if(eg[a][b]) tmp += dp[v][b];
}
dp[u][a] *= tmp;
}
}
}
int main() {
scanf("%d %d", &n, &m);
for(int i = 0; i < n; i++) lg[1 << i] = i;
for(int i = 1; i <= m; i++) {
int u, v; scanf("%d %d", &u, &v); --u; --v;
eg[u][v] = eg[v][u] = 1;
}
for(int i = 1; i < n; i++) {
int u, v; scanf("%d %d", &u, &v); --u; --v;
link(u, v); link(v, u);
}
LL ans = 0;
int all = (1 << n) - 1;
for(int i = 0; i < 1 << n; i++) {
dfs(0, -1, i);
for(int j = 0; j < n; j++) {
ans += (__builtin_popcount(all ^ i) & 1) ? -dp[0][j] : dp[0][j];
}
}
printf("%lld\n", ans);
}
本文来自博客园,作者:purplevine,转载请注明原文链接:https://www.cnblogs.com/purplevine/p/16925869.html