牛客国庆集训派对Day3 B Tree(树形dp + 组合计数)

《牛客国庆集训派对Day3 B Tree(树形dp + 组合计数)》

 

 

 

题意有点绕,其实就是让你求一个点能被多少个点集包含,同时这些点集内的点要相互连通。

首先,简单来说,如果只是计算一个有根树中任意一个点被多少个只包含它以及它的子树的点的点集包含,那么直接普通的树上统计的trick就可以搞定。但是现在问题是,点集的点可以是其子树之外的点。我们注意到,对于根来说,它的答案都满足点集里面的点都在它的子树里面,所以说如果能够把每一个点都当作根来计算一次,那么就可以简化问题。

我们令f[x]表示包含x而且点集里面的点都在x的子树里面的点集的个数。那么f[x]可以通过迭代以下式子得出:

                《牛客国庆集训派对Day3 B Tree(树形dp + 组合计数)》

可以看到,这个式子最后可以化简成x的儿子对应点集数加一的乘积,具体来说:

                                               《牛客国庆集训派对Day3 B Tree(树形dp + 组合计数)》

进行一次dfs我们就可以求出所有的f[x],而根的答案就是f[x]。之后,根据上面说的,我们要想办法把每一个点都当作根去计算一次。我们考虑再进行一次dfs,这次dfs满足在搜索到点x的时候,其父亲fa的答案已经算出,也即fa作为根的时候的答案已经算出。现在考虑计算x的答案,也即以x为根的答案。

如果把x看作根,那么与原本x的子树相比,x会多一个儿子,也即fa。根据上面的公式,原本的子树时候的答案是f[x],最后需要乘上一个(f[fa]+1)。但是这里f[fa]原本是x的父亲,这个里面的答案已经把x以及其子树考虑过了,所以我们要把这一部分去掉。去掉的方法也很简单,直接除以当前的(f[x]+1),也即乘以(f[x]+1)的逆元。那么,总的来说有转移方程:

                                          《牛客国庆集训派对Day3 B Tree(树形dp + 组合计数)》

这样,用两个dfs或者说树形dp,就可以求出最后的答案。

但是呢,本题特别的坑。注意到在第一遍dfs的时候,算出来的f[x]是一堆数字的乘积,而在第二遍dfs的时候,需要乘以一些逆元,也即把一些原本乘过的东西去掉。但是,如果当某个(f[son]+1)对mod取模之后,恰好等于0的话,f[x]就会一直等于0,在第二遍dfs的时候并不能通过乘以逆元的形式把这个乘以0的影响给去掉,这样就会产生错误。为了解决这个问题,我们对于每一个点维护一个flag,表示它的儿子中,有多少个的f[son]+1对mod取模之后等于0。然后在统计对应f[x]的时候不把等于零的儿子给乘进去。如果有儿子是0,删掉非零儿子的情况对结果无影响;删掉为零的儿子,且为零的儿子只有一个,那么在删掉这个儿子的时候不用乘以逆元,直接用对应的f[x]即可。如果没有儿子是0,那么直接乘以逆元即可,同时由于此时相当于把树旋转了,所以还要维护flag的变化。具体见代码:

#include<bits/stdc++.h>
#define PI 3.1415926535
#define mod 1000000007
#define LL long long
#define pb push_back
#define lb lower_bound
#define ub upper_bound
#define INF 0x3f3f3f3f
#define sf(x) scanf("%d",&x)
#define sc(x,y,z) scanf("%d%d%d",&x,&y,&z)
#define clr(x,n) memset(x,0,sizeof(x[0])*(n+5))
#define file(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout)
using namespace std;

const int N = 1000010;

int ls[N<<1],g[N<<1],nxt[N<<1],n,e;
LL f[N]; int flag[N];

inline void upd(LL &x,LL y){x=x*y%mod;}

void dfs1(int x,int fa)
{
    f[x]=1;
    for(int z=ls[x];z;z=nxt[z])
    {
        int y=g[z];
        if (y==fa) continue;
        dfs1(y,x);
        if ((flag[y]?0:f[y])+1==mod) flag[x]++;     //如果是0儿子,那么统计flag,不计入乘积
            else upd(f[x],(flag[y]?0:f[y])+1);
    }
}

LL qpow(LL x,LL n)
{
    LL res=1;
    while(n)
    {
        if (n&1) res=res*x%mod;
        x=x*x%mod; n>>=1;
    }
    return res;
}

void dfs2(int x,int fa)
{
    if (fa!=0)
        if ((flag[x]?0:f[x])+1==mod)
        {
            if (flag[fa]==1)            //如果有0儿子,且个数恰好是1,且x恰好就是则个0儿子
                if (f[fa]+1==mod) flag[x]++;        //还是要考虑维护flag
                     else upd(f[x],f[fa]+1);
        } else if (!flag[fa])            //如果没有0儿子,那么直接用逆元
        {
            LL tmp=f[fa]*qpow(1+(flag[x]?0:f[x]),mod-2)%mod+1;
            if (tmp==mod) flag[x]++; else upd(f[x],tmp);    //维护flag
        }
    for(int z=ls[x];z;z=nxt[z])
    {
        int y=g[z];
        if (y==fa) continue;
        dfs2(y,x);
    }
}

inline void add(int x,int y)
{
    g[++e]=y;
    nxt[e]=ls[x];
    ls[x]=e;
}

int main()
{
    sf(n);
    for(int i=1;i<n;i++)
    {
        int x,y; sf(x); sf(y);
        add(x,y); add(y,x);
    }
    dfs1(1,0);
    dfs2(1,0);
    for(int i=1;i<=n;i++)
        printf("%lld\n",flag[i]?0LL:f[i]);
    return 0;
}

 

    原文作者:B树
    原文地址: https://blog.csdn.net/u013534123/article/details/82934820
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞