【CF512D】Fox And Travelling(拓扑+树上背包)

点此看题面

  • 给定一张(n)个点(m)条边的图。
  • 访问图中一个点,需要满足与它直接相连的所有点中至多只有一个没被访问过。
  • 对于每个(kin[0,n]),你需要求出访问(k)个点的方案数。
  • (nle100)

拓扑去环

首先我们考虑,环上的点是永远不可能被访问的,且两个环之间的路径上的点也是永远不可能被访问的(因为它们度数始终大于等于(2))。

故我们完全可以从原图中先把所有环删去,而这一过程的实现可以用拓扑,因为环是不可能加入到拓扑序列中的。

删去环后原图就变成了森林,但是其中的树分无根树和有根树两类:

  • 如果这本来就是一棵树,就是无根树
  • 如果这本来与一个环相连,就以与环相连的点为根。

树上背包

有根树的树上背包应该是很容易的吧,转移方程就是:

[f_{x,j+k} exttt{+=}C_{j+k}^j imes f_{x,j} imes f_{y,k} ]

这里因为两个序列自身是有序的,所以它们合并的方案数只要乘上一个组合数就好了。

而对于无根树,我们以每个点为根都做一次树上背包。

那么考虑选(i)个点的每种方案会被重复计算多少次(假设树大小为(tot)),发现若(i ot=tot),则只要不以这(i)个点为根都会把该方案计算一遍。

也就是说,做无根树时,对于(i ot=tot),我们需要给最终的(f_i)除以(tot-i)

最后不同树之间的合并又是一个背包,转移和上面给出的方程基本一致,就不再赘述了。

代码:(O(n^3))

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 100
#define X 1000000009
using namespace std;
int n,m,C[N+5][N+5];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
struct Graph
{
	int ee,lnk[N+5];struct edge {int to,nxt;}e[N*N+5];
	I int& operator [] (CI x) {return lnk[x];}
	I edge& operator () (CI x) {return e[x];}
	I void add(CI x,CI y) {e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y;}
}G,P;
namespace Topo//拓扑
{
	int d[N+5],q[N+5],vis[N+5],IQ[N+5],rt[N+5],ty[N+5];
	I void MakeRt(CI x,CI Rt)//以Rt为根(对于无根树,方便起见也给一个根)
	{
		rt[x]=Rt;for(RI i=P[x];i;i=P(i).nxt) !rt[P(i).to]&&(MakeRt(P(i).to,Rt),0);
	}
	I void Work()
	{
		RI i,H=1,T=0;for(i=1;i<=G.ee;++i) ++d[G(i).to];//初始化度数
		for(i=1;i<=n;++i) d[i]<=1&&(IQ[q[++T]=i]=1);//初始化队列
		W(H<=T) for(vis[q[H]]=1,i=G[q[H++]];i;i=G(i).nxt)//拓扑去环
			!vis[G(i).to]&&--d[G(i).to]<=1&&!IQ[G(i).to]&&(IQ[q[++T]=G(i).to]=1);
		RI x,f;for(x=1;x<=T;++x) if(d[q[x]])
		{
			for(f=1,i=G[q[x]];i;i=G(i).nxt) IQ[G(i).to]&&(P.add(q[x],G(i).to),P.add(G(i).to,q[x]),f=0);//连边
			f&&(ty[q[x]]=1),IQ[q[x]]=0;//判断是否与环相连
		}
		for(x=1;x<=T;++x) ty[q[x]]&&(MakeRt(q[x],q[x]),0);for(x=1;x<=T;++x) !rt[q[x]]&&(MakeRt(q[x],q[x]),0);//优先建有根树,剩下的是无根树
	}
}
namespace Bag//树上背包
{
	int tot,f[N+5][N+5],f_[N+5],g[N+5],F[N+5];
	I void DP(CI x,CI lst=0)//树形DP
	{
		for(RI i=1;i<=g[x];++i) f[x][i]=0;f[x][g[x]=0]=1;
		for(RI i=P[x],j,k;i;i=P(i).nxt) if(P(i).to^lst)//树上背包板子,但注意组合数
		{
			for(DP(P(i).to,x),j=g[x];~j;--j) for(k=1;k<=g[P(i).to];++k)
				f[x][j+k]=(1LL*f[x][j]*f[P(i).to][k]%X*C[j+k][j]+f[x][j+k])%X;g[x]+=g[P(i).to];
		}
		++g[x],f[x][g[x]]=f[x][g[x]-1];//因为根只能放最后一个,可直接复制答案
	}
	I void Walk(CI x,CI lst=0)//以每个点为根做背包
	{
		RI i;for(i=P[x];i;i=P(i).nxt) P(i).to^lst&&(Walk(P(i).to,x),0);
		for(DP(x),i=1;i<=g[x];++i) f_[i]=(f_[i]+f[x][i])%X;
	}
	I void Solve(CI x,CI ty)//求解x所在的树,ty表示是否有根
	{
		RI i,j;if(ty) for(DP(x),i=1;i<=g[x];++i) f_[i]=f[x][i];//有根树直接DP
		else {for(i=1;i<=n;++i) f_[i]=0;for(Walk(x),i=1;i^g[x];++i) f_[i]=1LL*f_[i]*QP(g[x]-i,X-2)%X;}//无根树给f[i]除以g[x]-i
		for(i=tot;~i;--i) for(j=1;j<=g[x];++j) F[i+j]=(1LL*F[i]*f_[j]%X*C[i+j][i]+F[i+j])%X;tot+=g[x];//总背包统计所有答案
	}
}
int main()
{
	RI i,j,x,y;for(scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),G.add(x,y),G.add(y,x);
	for(C[0][0]=i=1;i<=n;++i) for(C[i][0]=j=1;j<=i;++j) C[i][j]=(C[i-1][j-1]+C[i-1][j])%X;//预处理组合数
	for(Topo::Work(),Bag::F[0]=i=1;i<=n;++i) i==Topo::rt[i]&&(Bag::Solve(i,Topo::ty[i]),0);//拓扑去环,然后分别处理每一棵树
	for(i=0;i<=n;++i) printf("%d
",Bag::F[i]);return 0;//输出总背包中的答案
}
原文地址:https://www.cnblogs.com/chenxiaoran666/p/CF512D.html