[BZOJ4784][UOJ290][ZJOI017]仙人掌-树形DP

仙人掌

Description

如果一个无自环无重边无向连通图的任意一条边最多属于一个简单环,我们就称之为仙人掌。所谓简单环即不经过重复的结点的环。

pic
现在九条可怜手上有一张无自环无重边的无向连通图,但是她觉得这张图中的边数太少了,所以她想要在图上连上一些新的边。同时为了方便的存储这张无向图,图中的边数又不能太多。经过权衡,她想要加边后得到的图为一棵仙人掌。

不难发现合法的加边方案有很多,可怜想要知道总共有多少不同的加边方案。两个加边方案是不同的当且仅当一个方案中存在一条另一个方案中没有的边。

Input

多组数据,第一行输入一个整数T表示数据组数。
每组数据第一行输入两个整数n,m,表示图中的点数与边数。
接下来m行,每行两个整数u,v(1≤u,v≤n,u!=v)表示图中的一条边。保证输入的图联通且没有自环与重边。
Sigma(n)<=5*10^5,m<=10^6,1<=m<=n*(n-1)/2

Output

对于每组数据,输出一个整数表示方案数,当然方案数可能很大,请对998244353取模后
输出。

Sample Input

2
3 2
1 2
1 3
5 4
1 2
2 3
2 4
1 5

Sample Output

2
8

对于第一组样例合法加边的方案有 {}, {(2,3)},共 2 种。


咱可是来学仙人掌的啊……
然后被标题骗进来了……
不过这还真是一道很棒的题呢~
如果是咱这种蒟蒻的话肯定是想不出来的呢~


思路:

让咱来全真模拟一下这道题应有的思路过程吧~

首先,看题发现完全不可做,决定水部分分。
于是我们把目光投向数据范围——
咦居然有为树的部分分,这么良心???

那么树咱该怎么做呢?
很显然,说到计数绝大多数情况都是DP,既然是树那就树形DP呗~
那么就是用树形DP处理仙人掌上每条边被覆盖的情况了~

然而这时发生了一个问题:
因为仙人掌上会有不存在与任一个环上的边,也就是说总共被覆盖的边数不确定,如果把这种情况算进去,就会变得很难表示状态不是吗?

通过观察题面咱得到了一个解法:
题面不是说没有重边吗?
那么我们把仙人掌上不属于任意一个环的边当做被自己的一条重边覆盖就好了。
就像这样:

 0
/ \
\ /
 0

这样就能使总共被覆盖的边数永远等于m了~

那就来DP吧~
令f[x]表示以x为根节点的子树中连好边的情况数。
那么分两种情况:

1.子树中没有向上连出子树的边(也就是根节点)
把所有直接相连的子节点处的答案乘起来,再乘上所有直接相连的子节点的子树之间两两配对的方案数即可。

2.子树中有一条向上连出的边(啥你说为什么只有一条?这特么可是棵树啊)
那么就是说有一条神奇的边并没有与其他的当前子树中的节点相连而是连出去了。
枚举这条边并对所得结果求和,咱可以发现一个特点:
这种情况可以表示为在上面的情况的基础上加入当前正在枚举的点与其子节点的子树匹配,能与当前正在枚举的点匹配就代表是连出去了。

于是咱先预处理出一个g[x]数组,表示x个节点之间两两配对的方案数。
得到一个递推式:

g[i]=g[i1]+g[i2](i1)

也就是说,加入一个新节点的方案数,等同于原有的i-1个节点正常匹配,当前加入节点不参与匹配,或者在原有i-2个节点中,有一个节点被挑选出来与新加入的节点匹配,其余i-2个正常匹配的方案数的和。

然后是求f[x]数组,这显然就是一个普通的树形DP了,初始为1,乘上每个直接相连的子节点的f值,最后如果是第一种情况就乘上下标为子节点数的g值,第二种就乘上下标为子节点数+1的g值,就可以了~

所以扯了这么久部分分有什么卯月啊……
当然有!
发现不是仙人掌的情况,输出0。
其他情况下,已有的环显然毫无意义——因为咱加边时根本不能碰它们,不然就不是一棵仙人掌了~
那么直接选择性无视这些环,把原图变成一个森林,对每棵树用部分分方法求解即可~

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<algorithm>

using namespace std;

typedef long long ll;

inline int read()
{
    int x=0;char ch=getchar();
    while(ch<'0' || '9'<ch)ch=getchar();
    while('0'<=ch && ch<='9')x=x*10+(ch^48),ch=getchar();
    return x;
}

const int N=5e5+9;
const int M=1e6+9;
const int md=998244353;

int to[M],nxt[M],beg[N],tot;
int n,m,fa[N],dep[N],dfn[N],path[N],a[N],dfs_clock;
ll f[N],g[N],ans;

inline void adde(int u,int v)
{
    to[++tot]=v;
    nxt[tot]=beg[u];
    beg[u]=tot;
}

inline void add(int u,int v)
{
    adde(u,v),adde(v,u);
}

void dfs(int u)
{
    dfn[u]=++dfs_clock;

    for(int i=beg[u],v;i;i=nxt[i])
        if((v=to[i])!=fa[u] && !dfn[v])
        {
            fa[v]=u;
            dep[v]=dep[u]+1;
            dfs(v);
        }
}

inline bool cmp(int a,int b)
{
    return dep[a]<dep[b];
}

void dfs2(int u,bool isrt)
{
    f[u]=1;
    path[u]=-1;
    int soncnt=0;

    for(int i=beg[u],v;i;i=nxt[i])
        if((v=to[i])!=fa[u] && path[v]==1)
        {
            soncnt++;
            dfs2(v,0);
            (f[u]*=f[v])%=md;
        }

    if(soncnt==0)
        return;

    if(isrt)
        (f[u]*=g[soncnt])%=md;
    else
        (f[u]*=g[soncnt+1])%=md;
}

int main()
{
    g[0]=g[1]=1;
    for(int i=2;i<N;i++)
        g[i]=(g[i-1]+g[i-2]*(i-1))%md;

    int T=read();
    while(T--)
    {
        n=read();
        m=read();

        tot=1;
        for(int i=1;i<=n;i++)
            beg[i]=fa[i]=dfn[i]=dep[i]=path[i]=0;

        for(int i=1;i<=m;i++)
            add(read(),read());

        tot=0;
        dep[1]=1;
        fa[1]=0;
        dfs(1);

        for(int i=1;i<=m;i++)
        {
            int st=to[i<<1],ed=to[i<<1|1];
            if(dfn[st]<dfn[ed])
                swap(st,ed);

            while(st!=ed)
            {
                path[st]++;
                if(path[st]>2)
                    goto wa;
                st=fa[st];
            }
        }

        if(0)
        {
            wa:;
            puts("0");
            continue;
        }

        for(int i=1;i<=n;i++)
            a[i]=i;
        sort(a+1,a+n+1,cmp);

        ans=1ll;
        for(int i=1;i<=n;i++)
            if(path[a[i]]!=-1)
            {
                dfs2(a[i],1);
                (ans*=f[a[i]])%=md;
            }

        printf("%lld\n",ans);
    }

    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值