【ZJOI 2016 小星星】【容斥 + 树形 DP】

本文介绍了一种结合容斥原理与树形动态规划的方法,用于解决特定类型的图论问题。通过定义状态f[i][j]表示以i为根的子树中,节点i映射到图中节点j的方案数,利用DFS遍历树结构,计算满足条件的映射方案总数。最终,通过容斥原理去除非法映射,得出合法方案的数量。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

容斥好题喵呜~

我们一共要满足两个限制:

  1. 树中的一个点对应图中一个点,且一一对应。
  2. 树中两点有边的,图中两点也对应有边。

O(nn) 我们可以直接暴力枚举每个点的对应关系,判断是否可以每个树中的点都对应到图中,如果满足,ans++

优化一些的暴力是用 f[i][j] 表示 i 为根的子树,使用了 j 所表示的点映射到图上的方案数。
第一次 dfs,预处理出 pre[i][j]=1 表示树中的 i 可以对应到图中的 j 中。
第二次 dfs,暴力合并。
枚举当前点 u 的初始状态(即 pre[u][x]=1 的所有 x)的子集 S,从 (以 u 为根的子树的子集) 转移到 (S 合并上 以 u 为根的子树的子集)。
形如:f[u][i|j]+=f[v][j]i 是 子集 Sj 为以 u 为根的子树的子集。
复杂度 O(3nn2),考虑优化。如果你有高深的卡常技巧好像也是可以过的。

如果我们放宽限制,只统计满足限制 2 的方案数,那么每个点映射的点构成的集合是可重集 S,也就是说树上两个不同点的映射可以相同,那么我们显然可以用树形 DP O(N3) 转移。
f[i][j] 表示树上 i 号点映射图中 j 号点的方案数,每次枚举根对应一个节点,再枚举每个儿子节点对应节点 f[k][l],乘起来就可以了,可以做到 O(n3) 计算。注意 k 的枚举一共只有 n 次。

但是我们无法满足限制 1 一一对应,那实际上不合法的状态可以被用两种方法表示:至少有一个点匹配了多个点;至少有一个点没有被匹配到。
这样的话就可以发现强制某些点不选就可以构造出需要去掉的不合法情况,方案数 = 所有点都可以匹配到 - 至少有1个点未被选 + 至少有2个点未被选 - 至少有3个点未被选 …(这里的图上的点可以被树上的点重复覆盖) ,即若至少有偶数个点未被选,就加上,否则减去。
那么对于 S,我们记 sum=Σf[root][j],jS,如果 n|S| 是偶数 ans 就加上,否则减去。(容斥)

我们将这个模型抽象出来:

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)|+,所以我们枚举 2n1 个并集,用树上 DP 计算出并集的大小,容斥一下就能得到 |A(1)A(2)A(n)| 了。
—— By wzj 大爷

总复杂度 O(2nn3)

复习一下:枚举子集的二进制写法

// 枚举 i 的子集
for (int j = i & (i - 1); j; j = i & (j - 1))

这样做就是每次不断 - 1 来枚举所有子集,它不是忽略了 i 中的 0,而是在 & i 的过程中将 0 消去了。

理解了这个枚举子集抽离点的部分就很容易了。

#include <bits/stdc++.h>
#define ll long long

using 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;   
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值