题目
给出一棵n(2<=n<=1e5)个点的树。
对于k从[1,n],分别解决如下问题:
考虑按照如下的顺序在树上的每一个点上写数字:
①在点k上写下数字1
②已经写数的点可以向相邻点扩展,扩展到一个还没写数的点,在该点上写一个还没写过的且最小的数,
如果有多个可以扩展,随机选取一个
③重复②过程,直至写完2到n
对于每一个k,求不同的写数方案,答案模1e9+7
思路来源
Codeforces群winterzz1 + Atcoder官方题解
题解
换根dp,一般用于sz[u]、sz[v]发生改变时,可以通过相邻的状态转移而来
把暴力的O(n^2)的树形dp,转化为O(n)的树形dp
两遍dfs,第一遍先求根的答案,第二遍通过换根求出所有点的答案
第一遍求根的答案的时候,
在只考虑子树内部方案的时候,dp[u]=dp[v]的乘积,其中v是u的子树内的点,
在考虑子树间的方案带来的顺序的时,相当于把内部方案看成相同的数
比如,4个1,5个2,6个3构成一个长度为15的序列,方案有多少种,
答案是随便排的方案数除掉每个的内部顺序的方案数,
第一次dfs,回溯的时候,dp[1]的答案有了,
第二次dfs的时候,起始u是v的父亲,切断u和v之间的连边,然后把u挂到v的子树上,起到了换根的过程,
注意回溯的时候,再把根换回来
每次sz发生变化时,先除掉原来的,再乘上一个新的,
本题为了快速转移,要预处理阶乘、阶乘的逆元
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define pb push_back
const int N=2e5+10,mod=1e9+7;
vector<int>e[N];
int sz[N],n,u,v;
ll dp[N],fac[N],Finv[N],inv[N],ans[N];
ll modpow(ll x,ll n,ll mod)
{
ll res=1;
for(;n;x=x*x%mod,n/=2)
if(n&1)res=res*x%mod;
return res;
}
ll get_inv(ll x)
{
return modpow(x,mod-2,mod);
}
void init(int n)//n<N
{
inv[1]=1;
for(int i=2;i<=n;++i)inv[i]=((mod-mod/i)*inv[mod%i])%mod;
fac[0]=Finv[0]=1;
for(int i=1;i<=n;++i)fac[i]=fac[i-1]*i%mod,Finv[i]=Finv[i-1]*inv[i]%mod;
//Finv[n]=modpow(jc[n],mod-2,mod);
//for(int i=n-1;i>=1;--i)Finv[i]=Finv[i+1]*(i+1)%mod;
}
//视具体情况更改link/cut/leaf函数
void dp_link(int rt1,int rt2)
{
//因sz发生变化 除掉原来的(sz-1)! 乘上新的(sz-1)!
dp[rt1]=dp[rt1]*Finv[sz[rt1]-1]%mod;
sz[rt1]+=sz[rt2];
dp[rt1]=dp[rt1]*fac[sz[rt1]-1]%mod;
//乘上dp[rt2]/(sz[rt2]!)
dp[rt1]=dp[rt1]*Finv[sz[rt2]]%mod;
dp[rt1]=dp[rt1]*dp[rt2]%mod;
}
void dp_cut(int rt1,int rt2)
{
//因sz发生变化 除掉原来的(sz-1)! 乘上新的(sz-1)!
dp[rt1]=dp[rt1]*Finv[sz[rt1]-1]%mod;
sz[rt1]-=sz[rt2];
dp[rt1]=dp[rt1]*fac[sz[rt1]-1]%mod;
//除掉dp[rt2]/(sz[rt2!]
dp[rt1]=dp[rt1]*fac[sz[rt2]]%mod;
dp[rt1]=dp[rt1]*get_inv(dp[rt2])%mod;
}
void get_leaf(int rt)
{
sz[rt]=1;
dp[rt]=1;
}
///-----------------------------
void change_rt(int rt1,int rt2)//把根从rt1切换到rt2
{
dp_cut(rt1,rt2);// 使rt1失去儿子rt2
dp_link(rt2,rt1);//使rt2加上儿子rt1
}
void dp_dfs(int x,int fa)
{
get_leaf(x);
for(auto &i:e[x])
{
if(i!=fa)
{
dp_dfs(i,x);
dp_link(x,i);//回溯时 把x挂到i上
}
}
return;
}
void dfs(int x,int fa)
{
ans[x]=dp[x];
for(auto &i:e[x])
{
if(i!=fa)
{
change_rt(x,i);//根从x换到i
dfs(i,x);
change_rt(i,x);//回溯 把根换回来
}
}
return;
}
int main()
{
scanf("%d",&n);
init(200000);
for(int i=1;i<n;++i)
{
scanf("%d %d",&u,&v);
e[u].pb(v),e[v].pb(u);
}
dp_dfs(1,-1);
dfs(1,-1);
for(int i=1;i<=n;++i)
{
printf("%lld\n",ans[i]);
}
return 0;
}
树形DP与换根技巧
本文介绍了一种树形动态规划(DP)算法,利用换根技巧将O(n^2)的时间复杂度优化至O(n),适用于解决树上点的编号方案计数问题。通过两次深度优先搜索(DFS),首次求解根节点的答案,随后通过换根策略求得所有节点的解决方案。文章详细解释了如何在节点间进行状态转移,并提供了完整的C++代码实现。

被折叠的 条评论
为什么被折叠?



