【CF512D】Fox And Travelling(拓扑+树上背包)
- 给定一张\(n\)个点\(m\)条边的图。
- 访问图中一个点,需要满足与它直接相连的所有点中至多只有一个没被访问过。
- 对于每个\(k\in[0,n]\),你需要求出访问\(k\)个点的方案数。
- \(n\le100\)
拓扑去环
首先我们考虑,环上的点是永远不可能被访问的,且两个环之间的路径上的点也是永远不可能被访问的(因为它们度数始终大于等于\(2\))。
故我们完全可以从原图中先把所有环删去,而这一过程的实现可以用拓扑,因为环是不可能加入到拓扑序列中的。
删去环后原图就变成了森林,但是其中的树分无根树和有根树两类:
- 如果这本来就是一棵树,就是无根树。
- 如果这本来与一个环相连,就以与环相连的点为根。
树上背包
有根树的树上背包应该是很容易的吧,转移方程就是:
\[f_{x,j+k}\texttt{+=}C_{j+k}^j\times f_{x,j}\times f_{y,k}
\]
这里因为两个序列自身是有序的,所以它们合并的方案数只要乘上一个组合数就好了。
而对于无根树,我们以每个点为根都做一次树上背包。
那么考虑选\(i\)个点的每种方案会被重复计算多少次(假设树大小为\(tot\)),发现若\(i\not=tot\),则只要不以这\(i\)个点为根都会把该方案计算一遍。
也就是说,做无根树时,对于\(i\not=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\n",Bag::F[i]);return 0;//输出总背包中的答案
}
待到再迷茫时回头望,所有脚印会发出光芒