ZJOI2017 仙人掌 转化模型后的简单树形dp

题目大意

给定一个 n 个点,m条变的无向无自环的连通图,问都多少种加边方案使得加完边的图是一幅没有重边仙人掌。(即满足任意一条边只属于一个简单环中的无向无自环图的连通图)
多组数据。

n5105
m106

解题思路

首先讨论给定的图是树的情况。

(转化一)
要求不能有边存在与两个简单环内相当于我们要加入新的边去覆盖这个树,是的每条边最多被覆盖一次。

由于每条边不一定被覆盖,所以比较麻烦。注意到题目的一个性质,不能存在重边,那么意味着我们可以将原来构出仙人掌中不存在环内的边视为向原来树中的父亲连了一条边,这个与原问题是等价的。这样就有了每条树边都要被覆盖一次的限制。

(转化二)
问题就转化成了有多少种覆盖方案使得树上每条边恰好被覆盖一次。

这就很好做了,设
f[i] 表示做完以 i 为根的子树,且没有路径可以向上扩展。
g[i]表示做完以 i 为根的子树,且有路径可以向上扩展。
h[i]表示有i个点,变成若干个两个点和一个点组合的方案数。

转移就很显然了。
h[0]=h[1]=1
h[i]=h[i1]+h[i2](i1)(i>2)
f[i]=(json[i]g[j])h[|son[i]|]
g[i]=f[i]+(json[i]g[j])h[|son[i]|1]|son[i]|
(分 i <script type="math/tex" id="MathJax-Element-585">i</script>节点是否为向上覆盖边的端点讨论)

对于原图不是树的情况可以先把原图不是仙人掌的情况判掉,答案肯定是0。由于原图的环是不能被任何边覆盖的,所以直接把环上的边删掉,转化成若干棵树即可。

程序

//YxuanwKeith
#include <cstring>
#include <cstdio>
#include <algorithm>

using namespace std;

const int MAXN = 5e5 + 5, MAXM = 1e6 + 5;
const int Mo = 998244353;

struct Path {
    int u, v;
} path[MAXN];

int n, m, tim, bel[MAXN], deep[MAXN], fa[MAXN], f[MAXN], g[MAXN], h[MAXN], tag[MAXN];
int tot, Last[MAXN], Next[MAXM * 2], Go[MAXM * 2];
bool flag[MAXN];

void link(int u, int v) {
    Next[++ tot] = Last[u], Last[u] = tot, Go[tot] = v;
}

void dfs(int now, int pre) {
    flag[now] = 1, fa[now] = pre;
    deep[now] = deep[pre] + 1;
    bel[++ tim] = now;
    for (int p = Last[now]; p; p = Next[p]) 
        if (!flag[Go[p]]) dfs(Go[p], now);
}

bool remake() {
    dfs(1, 0);
    for (int i = 1; i <= n; i ++) flag[i] = 0;
    for (int i = 1; i <= m; i ++) {
        int u = path[i].u, v = path[i].v;
        if (deep[u] < deep[v]) swap(u, v);
        if (fa[u] == v) continue;
        tag[u] ++, tag[v] --;
    }
    for (int i = n; i; i --) {
        int now = bel[i];
        for (int p = Last[now]; p; p = Next[p]) {
            int v = Go[p];
            if (fa[v] != now) continue;
            tag[now] += tag[v];
        }
        if (!tag[now]) flag[now] = 1; 
        if (tag[now] > 1) return printf("0\n"), 0;
    }
    for (int i = 1; i <= n; i ++) Last[i] = 0;
    tot = 0;
    for (int i = 1; i <= n; i ++) 
        if (flag[i] && fa[i]) link(i, fa[i]), link(fa[i], i);
    return 1;
}

void dp(int now, int pre) {
    flag[now] = 1;
    f[now] = 1, g[now] = 0;
    int num = 0;
    for (int p = Last[now]; p; p = Next[p]) {
        int v = Go[p];
        if (v == pre) continue;
        dp(v, now);
        f[now] = 1ll * f[now] * g[v] % Mo;
        num ++;
    }
    g[now] = (1ll * f[now] * h[num] % Mo + 1ll * f[now] * h[num - 1] % Mo * num % Mo) % Mo;
    f[now] = 1ll * f[now] * h[num] % Mo;
}

void solve() {
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i ++) Last[i] = tag[i] = flag[i] = 0;
    h[0] = h[1] = 1;
    for (int i = 2; i <= n; i ++) 
        h[i] = (h[i - 1] + 1ll * h[i - 2] * (i - 1) % Mo) % Mo;
    tot = tim = 0;
    for (int i = 1; i <= m; i ++) {
        int u, v;
        scanf("%d%d", &u, &v);
        link(u, v), link(v, u);
        path[i].u = u, path[i].v = v;
    }
    if (!remake()) return;
    for (int i = 1; i <= n; i ++) flag[i] = 0;
    int ans = 1;
    for (int i = 1; i <= n; i ++) {
        if (flag[i]) continue; 
        dp(i, 0);
        ans = 1ll * ans * f[i] % Mo;
    }
    printf("%d\n", ans);
}

int main() {
    int t;
    scanf("%d", &t);
    for (int i = 1; i <= t; i ++) solve();
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值