[zjoi2017]仙人掌

前言

谨以此题纪念我第一次参加省选时刚了5h这一题得到0分的经历

题目相关

链接

题目大意

给出仙人掌定义:如果一个无自环无重边无向连通图的任意一条边最多属于一个简单环,我们就称之为仙人掌
给出一个图,求有多少种加边方式使得图成为一个仙人掌

数据范围

多组数据
∑ n ≤ 5 ∗ 1 0 5 , ∑ m ≤ 1 0 6 \sum n\le5*10^5,\sum m\le10^6 n5105,m106

题解

首先,如果这幅图本身就已经不是仙人掌了,那么输出 0 0 0
如何判断呢?我们发现,建圆方树的时候,两条圆点的连边最多只会被删除一次,如果被删除了多次那么说明这幅图不是仙人掌

对于仙人掌的简单环上的边,我们一定不可以进行一条连边使得两个端点之间有一条路径经过这条边(因为这样就会出现一条边在多个简单环中的情况)
那么我们就直接把仙人掌简单环上的边直接删除,容易发现剩下的都是一些树了

对于树的情况,我们发现有两个东西不好处理:一个是最终的图中可以有边不在简单环上,二是添加的边不能有重边

我们发现这两个问题合在一起就被解决了,我们可以转化成:每条边都必须要在一个简单环上,允许有重边。容易发现,这么转化的答案不变

现在相当于是对于一棵树,求其被链覆盖的方案数

考虑树形dp

我们定义 f i f_i fi为以 i i i节点所在子树及其通往父亲的边被链覆盖的方案数
s o n u son_u sonu u u u节点的儿子集合, S ( n ) S(n) S(n) n n n个点、每个点的度数小于等于 1 1 1的图的数量
我们发现 f u = S ( ∣ s o n u ∣ + 1 ) ∗ ∏ v ∈ s o n u f v f_u=S(|son_u|+1)*\prod_{v\in son_u}f_v fu=S(sonu+1)vsonufv
特殊的,我们发现根节点没有父亲边,这样的话其 f i f_i fi的定义不包含通往父亲的边,那么 S ( x ) S(x) S(x)里面的 x x x就是儿子数量了

我们现在考虑怎么求 S ( n ) S(n) S(n)
我们考虑已知 S ( 1 ) S(1) S(1)~ S ( n ) S(n) S(n),怎么求 S ( n + 1 ) S(n+1) S(n+1)
我们考虑第 n + 1 n+1 n+1个点
如果它不连边,那么方案数为 S ( n ) S(n) S(n)
如果它连边,在前面 n n n个点中选择一个连,那么方案数就为 S ( n − 1 ) ∗ n S(n-1)*n S(n1)n
即得到递推式 S ( n + 1 ) = S ( n ) + n ∗ S ( n − 1 ) S(n+1)=S(n)+n*S(n-1) S(n+1)=S(n)+nS(n1)
这样,我们就能 O ( n ) \mathcal O(n) O(n)的完成此题

代码

#include
#include
#include
#include
namespace fast_IO
{
	const int IN_LEN=1000000,OUT_LEN=1000000;
	char ibuf[IN_LEN],obuf[OUT_LEN],*ih=ibuf+IN_LEN,*oh=obuf,*lastin=ibuf+IN_LEN,*lastout=obuf+OUT_LEN-1;
	inline char getchar_(){return (ih==lastin)&&(lastin=(ih=ibuf)+fread(ibuf,1,IN_LEN,stdin),ih==lastin)?EOF:*ih++;}
	inline void putchar_(const char x){if(oh==lastout)fwrite(obuf,1,oh-obuf,stdout),oh=obuf;*oh++=x;}
	inline void flush(){fwrite(obuf,1,oh-obuf,stdout);}
}
using namespace fast_IO;
#define getchar() getchar_()
#define putchar(x) putchar_((x))
#define rg register
typedef long long ll;
template<typename T>inline void read(T&x){char cu=getchar();x=0;bool fla=0;while(!isdigit(cu)){if(cu=='-')fla=1;cu=getchar();}while(isdigit(cu))x=x*10+cu-'0',cu=getchar();if(fla)x=-x;}
template<typename T>inline void printe(const T x){if(x>=10)printe(x/10);putchar(x%10+'0');}
template<typename T>inline void print(const T x){if(x>=0)printe(x);else putchar('-'),printe(-x);}
template <typename T> inline T min(const T a,const T b){return a<b?a:b;}
template <typename T> inline T abs(const T a){return a>0?a:-a;}
const int maxn=500005,maxm=2000005,mod=998244353;
int T,n,m;
int head[maxn],nxt[maxm],tow[maxm],tmp;bool del[maxm],DIE,VVV[maxm],vis[maxn];
inline void addb(const int u,const int v)
{
	tmp++;
	nxt[tmp]=head[u];
	head[u]=tmp;
	tow[tmp]=v;
	del[tmp]=VVV[tmp>>1]=0;
}
int stack[maxn],top,H;
void dfs(const int u)
{
	if(DIE)return;
	vis[u]=1;
	for(rg int i=head[u];i&&!DIE;i=nxt[i])
		if(!VVV[i>>1])
		{
			VVV[i>>1]=1;
			const int v=tow[i];
			stack[++top]=i;
			if(vis[v])
			{
				H=top;
				for(rg int j=top-1;j&&tow[stack[j]]!=v;j--)H=j;
				for(rg int j=top;j>=H;j--)
				{
					const int id=stack[j];
					if(del[id])DIE=1;
					del[id]=del[id^1]=1;
				}
			}
			else dfs(v);
			top--;
		}
}
ll S[maxn],f[maxn],ans;
void DFS(const int u,const int fa)
{
	int size=0;
	f[u]=1,vis[u]=1;
	for(rg int i=head[u];i;i=nxt[i])
		if(!del[i])
		{
			const int v=tow[i];
			if(v!=fa)DFS(v,u),f[u]=f[u]*f[v]%mod,size++;
		}
	if(fa)size++;
	f[u]=f[u]*S[size]%mod;
}
int main()
{
	S[0]=S[1]=1;
	for(rg int i=2;i<=500000;i++)S[i]=(S[i-1]+S[i-2]*(i-1))%mod;
	read(T);
	while(T--)
	{
		tmp=1;
		read(n),read(m);top=0;
		for(rg int i=1;i<=m;i++)
		{
			int u,v;read(u),read(v);
			addb(u,v),addb(v,u);
		}
		DIE=0,dfs(1);
		if(DIE)ans=0;
		else
		{
			ans=1;
			for(rg int i=1;i<=n;i++)vis[i]=0;
			for(rg int i=1;i<=n;i++)
				if(!vis[i])
				{
					int d=0;
					for(rg int j=head[i];j;j=nxt[j])
						if(!del[j])
							d++;
					if(d==1)
					{
						DFS(i,0);
						ans=ans*f[i]%mod;
					}
				}
		}
		print(ans),putchar('\n');
		for(rg int i=1;i<=n;i++)head[i]=0,vis[i]=0;
	}
	return flush(),0;
}

总结

其实和仙人掌没啥关系,就是很清真的树形dp,被题目名劝退了
然后转化问题的思路比较巧妙
最后,祈祷今年浙江省选RP++吧

你可能感兴趣的:(OI,仙人掌,dp)