Luogu P3687 [ZJOI2017]仙人掌 题解
首先这题巨坑,我们来看一看题意:
给出一张简单无向连通图,求有多少种加边方案可以使得加边后这张图是一棵仙人掌。
注意这题没有保证给出的图是一个仙人掌!那么这就意味着我们还得先判断它是不是一个仙人掌。
也就是说我们需要判断一张图是不是一个仙人掌,然后将环上的边删去。
有一种简单的方法是用tarjan求边双连通分量,tarjan时判断这棵树是否是仙人掌,tarjan后再把两个端点不在同一连通分量中的边删去。
然后我们就能得到若干棵树了,对每棵树进行树形dp,在每个结点处考虑非树边的情况,可以选择某一个儿子往上连一条边,也可以将两个儿子配对连边。其中将儿子两两配对连边的方案数可以预处理,用递推的方法求:
\(g[i]=g[i-1]+(i-1)*g[i-2]\)
然后把每棵树根处的dp值乘起来就是最终的答案了。
代码:
#include<bits/stdc++.h>
using namespace std;
#define N 1000007
#define M 2000007
#define ll long long
const int inf=0x3f3f3f3f;
const ll mod=998244353;
int hd[N],pre[M],to[M],num,dfn[N],ord,low[N],cl[N],st[N],tp;
ll f[N][2],g[N];
bool vis[N],del[M],flag;
void adde(int x,int y)
{
num++;pre[num]=hd[x];hd[x]=num;to[num]=y;
}
void tarjan(int v,int fa)
{
low[v]=dfn[v]=++ord;
st[++tp]=v;
bool bl=0;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==fa)continue;
if(!dfn[u])
{
tarjan(u,v);
if(low[u]<dfn[v])flag|=bl,bl=1;
low[v]=min(low[v],low[u]);
}
else
{
if(dfn[u]<dfn[v])flag|=bl,bl=1;
low[v]=min(low[v],dfn[u]);
}
}
if(low[v]==dfn[v])
{
while(st[tp]!=v)cl[st[tp--]]=v;
cl[st[tp--]]=v;
}
}
void dp(int v,int fa)
{
vis[v]=1;
int s=0;
ll sum=1;
for(int i=hd[v];i;i=pre[i])
{
int u=to[i];
if(u==fa||del[i])continue;
dp(u,v);s++;
sum=sum*(f[u][0]+f[u][1])%mod;
}
f[v][0]=sum*g[s]%mod;
if(s)f[v][1]=s*sum%mod*g[s-1]%mod;
else f[v][1]=0;
}
int main()
{
//freopen("data.in","r",stdin);
//freopen("test.out","w",stdout);
int t,n,m,x,y;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
//n=100;
ord=0;
for(int i=1;i<=n;i++)hd[i]=0,vis[i]=0,dfn[i]=low[i]=cl[i]=0;
g[0]=1;g[1]=1;
for(int i=2;i<=n;i++)g[i]=(g[i-1]+(i-1)*g[i-2])%mod;
//for(int i=1;i<=10;i++)printf("%lld\n",g[i]);return 0;
num=1;
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
adde(x,y),adde(y,x);
}
for(int i=1;i<=num;i++)del[i]=0;
flag=0;
tarjan(1,0);
if(flag)
{
printf("%d\n",0);
continue;
}
ll ans=1;
for(int v=1;v<=n;v++)
for(int i=hd[v];i;i=pre[i])
if(cl[v]==cl[to[i]])del[i]=1;
for(int i=1;i<=n;i++)
{
if(!vis[i])
{
dp(i,0);
ans=ans*f[i][0]%mod;
}
}
printf("%lld\n",ans);
}
return 0;
}