Loading

[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|}\)

这时的转移等均平凡。

\[dp_{u,i,S} \gets dp_{v,j,S}, \ j \in S,(i,j) \in E \]

初始化时把所有含有 \(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);
}
posted @ 2022-11-25 17:33  purplevine  阅读(23)  评论(0编辑  收藏  举报