题解 - 牛客挑战赛30 - C 小G砍树
题目意思
- 小G砍树
- 给你一个
n个节点的树,每次只能删除度为
1的节点问删光所有节点的总方案数。
-
n≤105
Sol
- 前置知识:换根DP,组合数
- 我们如果把状态设为
fu表示最后一个删数
u节点的总方案数。但是子树之间有相互影响的关系,做不了。但是我们考虑一定是从下往上删的,其实往上删节点的过程可以认为是一个填编号的过程。我们设
Sizu为
u节点的子树大小,那么
u节点的编号肯定为
Sizu,那么
u子树里的点(直接儿子)是可以随意填的。那么我们可以先预处理出
fu表示
u以及他的直接儿子都标记好标号的方案数。
- 那么
fu=∏v∈ufv×CSizu−SizvSizv,每次选一个儿子
v,要使
Sizu−=Sizv
- 以上都还算比较好理解,那么我们就开始利用换根
DP来求出每个点的方案数
gu,那么答案就是
ans=∑i=1ngi
- 其他博客没有清楚地讲换根的过程,我这里来讲一下:首先我们设
u为当前子树的根节点
v为即将要换根的节点,我们要如何从
u→v呢?
- 我么首先把
v对
u的贡献从
u中解除,这个比较好处理就是
v→u=fu×(Cn−1sizv×fv1),因为在预处理时候加进去的要减掉。然后我们现在以
v为根了要加
u→v贡献,那么
u→v=fv×Cn−1n−sizv×(v→u)。因为现在
v成为根了那么要把它新增子树的贡献加上去,这个可以和一开始的求方案树的柿子联系在一起思考。
- 这样我们就做完了这道好题。
Code
#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
inline int read()
{
int sum=0,ff=1; char ch=getchar();
while(!isdigit(ch))
{
if(ch=='-') ff=-1;
ch=getchar();
}
while(isdigit(ch))
sum=sum*10+(ch^48),ch=getchar();
return sum*ff;
}
const int N=1e5+5;
const int mo=998244353;
int n,m,ans,siz[N],f[N],inv[N],jc[N];
vector<int> G[N];
inline int ksm(int x,int y)
{
int ret=1ll;
while(y)
{
if(y&1) ret=ret*x%mo;
x=x*x%mo;
y>>=1ll;
}
return ret;
}
inline int C(int x,int y)
{
return jc[x]%mo*inv[x-y]%mo*inv[y]%mo;
}
inline void dfs(int u,int fa)
{
f[u]=1,siz[u]=1;
for ( int i=0;i<G[u].size();i++ )
{
int v=G[u][i];
if(v==fa) continue;
dfs(v,u);
siz[u]+=siz[v];
}
int sum=0;
for ( int i=0;i<G[u].size();i++ )
{
int v=G[u][i];
if(v==fa) continue;
f[u]=(f[u]*C(siz[u]-sum-1,siz[v])%mo*f[v]+mo)%mo;
sum+=siz[v];
}
}
inline void dp(int u,int fa)
{
for ( int i=0;i<G[u].size();i++ )
{
int v=G[u][i];
if(v==fa) continue;
int las=(f[u]*ksm(C(n-1,siz[v]),mo-2)%mo)*ksm(f[v],mo-2)%mo;
f[v]=f[v]*C(n-1,n-siz[v])%mo*las%mo;
dp(v,u);
}
}
signed main()
{
n=read();
jc[0]=inv[0]=1;
for ( int i=1;i<=n;i++ ) jc[i]=jc[i-1]*i%mo;
for ( int i=1;i<=n;i++ ) inv[i]=ksm(jc[i],mo-2)%mo;
for ( int i=1;i<n;i++ )
{
int x,y;
x=read(),y=read();
G[x].pb(y);
G[y].pb(x);
}
dfs(1,0);
dp(1,0);
for ( int i=1;i<=n;i++ ) ans=(ans+f[i])%mo;
printf("%lld\n",ans);
return 0;
}