AtCoder Beginner Contest 160 F.Distributing Integers(组合数学+换根dp)

题目

给出一棵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]={(sz[u]-1)!}*\prod_{v\epsilon son[u]}\frac{dp[v]}{sz[v]!}

在只考虑子树内部方案的时候,dp[u]=dp[v]的乘积,其中v是u的子树内的点,

在考虑子树间的方案带来的顺序的时,相当于把内部方案看成相同的数

比如,4个1,5个2,6个3构成一个长度为15的序列,方案有多少种,

答案是随便排的方案数除掉每个的内部顺序的方案数,\frac{15!}{4!5!6!}

第一次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;
}

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值