【牛客】乃爱与城市拥挤程度 — 树形dp,up and down

Deadly 提交于 2019-12-04 08:47:20

我太难了

这题做得我要死了,来来回回写了大概八九个小时
错误的原因要么是快速幂写错(一生之敌,要么是忘取模爆\(longlong\)变负数\(QAQ\)

链接:https://ac.nowcoder.com/acm/contest/1100/B
来源:牛客网

题目描述

乃爱天下第一可爱!

乃爱居住的国家有n座城市,这些城市与城市之间有n-1条公路相连接,并且保证这些城市两两之间直接或者间接相连。

我们定义两座城市之间的距离为这两座城市之间唯一简单路径上公路的总条数。

当乃爱位于第x座城市时,距离城市x距离不大于k的城市中的人都会认为乃爱天下第一可爱!

认为乃爱天下第一可爱的人们决定到乃爱所在的城市去拜访可爱的乃爱。我们定义这些城市的拥挤程度为:

距离城市x距离不大于k的城市中的人到达城市x时经过该城市的次数。例如:

假设\(k=2\),乃爱所在的城市是1号城市,树结构如上图所示时,受到影响的城市为\(1,2,3,4,5\),因为五个城市距离1号城市的距离分别为:\(0,1,2,2,2\),所以这五个城市都会认为乃爱天下第一。

\(1\)号城市到\(1\)号城市经过了\(1\)号城市。

\(2\)号城市到\(1\)号城市经过了\(1\)号、\(2\)号城市。

\(3\)号城市到\(1\)号城市经过了\(1\)号、\(2\)号、\(3\)号城市。

\(4\)号城市到\(1\)号城市经过了\(1\)号、\(2\)号、\(4\)号城市。

\(5\)号城市到\(1\)号城市经过了\(1\)号、\(2\)号、\(5\)号城市。

所以\(1\)号城市的拥挤程度是\(5\)\(2\)号城市的拥挤程度是\(4\)\(3\)号、\(4\)号、\(5\)号城市的拥挤程度都是1。

现在小\(w\)想要问你当乃爱依次位于第\(1、2、3、4、5...n\)座城市时,有多少座城市中的人会认为乃爱天下第一,以及受到影响城市的拥挤程度的乘积,由于这个数字会很大,所以要求你输出认为乃爱天下第一的城市拥挤程度乘积\(mod 10^9+7\)后的结果。

输入描述:

第一行是两个正整数\(n,k\)表示城市数目,以及距离乃爱所在城市距离不大于\(k\)的城市中的人认为乃爱天下第一!

接下来\(n-1\)行,每行两个正整数\(u,v\),表示树上一条连接两个节点的边。

输出描述:

输出两行。

第一行\(n\)个整数,表示当乃爱依次位于第\(1、2、3、4、5...n\)座城市时,有多少座城市中的人会认为乃爱天下第一。

第二行\(n\)个整数,表示当乃爱依次位于第\(1、2、3、4、5...n\)座城市时,受影响的城市拥挤程度乘积\(mod 10^9+7\)后的结果。

备注:

本题共有\(10\)组测试点数据。

对于前\(10\%\)的测试点满足 \(1\leqslant n\leqslant 10,1\leqslant k\leqslant 10\),树结构随机生成。

对于前\(30\%\)的测试点满足\(1\leqslant n\leqslant 10^3,1\leqslant k\leqslant 10\),树结构随机生成。

对于前\(70\%\)的测试点满足\(1\leqslant n\leqslant 10^5,1\leqslant k\leqslant 10\),树结构随机生成。

对于前\(100\%\)的测试点满足\(1\leqslant n\leqslant 10^5,1\leqslant k\leqslant 10\),树结构为手动构造。

对于测试点\(4\),在满足其前\(70\%\)的测试点条件下,额外满足\(k=1\)

对于测试点\(5\),在满足其前\(70\%\)的测试点条件下,额外满足\(k=2\)

对于测试点\(10\),在满足其前\(100\%\)的测试点条件下,额外满足树结构退化成一条链。

\(T2\)大样例下载链接:
https://pan.baidu.com/s/1AbBuEC8SmWlRMvtj6nLRkw

题解

这道题的每个文字都在传达着这道题的做法(雾:

首先,读完题以后,我们可以知道这道题毫无疑问是要在树上进行一些操作
然后我们发现这道题是要先处理子孙然后再处理自己的信息的,也就是要递归处理的,因为我们求离某个城市\(i\)的距离为\(k\)的城市数量,我们就必须知道离城市\(i\)的儿子的距离为\(k - 1\)的城市的信息,也就是\(add_{i,k} = \sum_{j = son} add_{j,k-1}\)
同样的,我们求离某个城市\(i\)的距离为\(k\)的城市拥挤度之积,同样也要递归求解,即\(mul_{i,k} = \prod_{j = son} mul_{j, k - 1}\),然后再乘它自己的拥挤度。

看出以上过程的话,思路就很清晰了,这是一道典型的树形\(dp\)
11又因为我们需要知道以每个点为根的深度为\(k\)的子树的信息,所以自然而然的想到\(up\) \(and\) \(down\)或者换根法(为什么?我马上讲
这里我用的是\(up\) \(and\) \(down\),所以接下来着重讲一下这种做法

先上一张图:

\(emm...\)确实有些可怕(逃

如图,\(0\)是根节点

我接下来用不同的颜色标注一下以\(2\)为根和以\(0\)为根用上述方法求出的\(2\)的深度为\(k = 2\)的子树有何不同,宁是否能看出一些猫腻

红色的是以\(0\)为根,蓝色的是以\(2\)为根,如图:

您一定会想,我这不是觉得宁是傻子吗,这么明显肯定看的出来啊,很明显以\(0\)为根时我们少统计了\(2\)的父节点(也就是\(0\)的信息

那这可怎么办呢?

宁可能会想,每个节点跑一遍不就好了吗,简单而粗暴,可是,宁看一眼数据范围,怕不是会\(T\)的很难看

那怎么办呢,如何只以\(0\)为节点\(dfs\)就可以求出呢?

一遍\(dfs\)一定是不可以的,我们考虑第二次\(dfs\)去完善之前的信息

我把刚刚那个图抽出一部分来反过来看,宁看看宁能否看出什么

可以发现我们漏的那部分就是以\(2\)的父亲(也就是\(0\)为根的深度为\(k - 1\)的子树,减去\(dp_{i, k - 2}\)

如果上面看的很绕的话,我说些人话(雾

设置四个数组:
\(dpadd[i][k]\) : 第一次\(dfs\)搞出的以\(0\)为根的树中 每个节点的深度为\(k\)的子树的大小

\(dpmul[i][k]\) : 第一次\(dfs\)搞出的以\(0\)为根的树中 每个节点的深度为\(k\)的子树的乘积大小

\(ansadd[i][k]\) : 第二次\(dfs\)搞出的以\(i\)为根的子树中 每个节点的深度为\(k\)的子树的大小 ( 也就是真正的答案

\(ansmul[i][k]\) : 第二次\(dfs\)搞出的以\(i\)为根的子树中 每个节点的深度为\(k\)的子树的乘积大小 ( 也就是真正的答案

那么我们上面的推导过程写成状态转移方程就是:

//在第一次dfs时
dpadd[now][j] = ( dpadd[now][j] + dpadd[v][j - 1] ) % MOD;

dpmul[now][j] = ( dpmul[now][j] * dpmul[v][j - 1] ) % MOD;
//在第二次dfs时
ansadd[now][i] = ( ansadd[fa][i - 1] + dpadd[now][i] - dpadd[now][i - 2] ) % MOD;

ansmul[now][i] = ( ( ansmul[fa][i - 1] * qpow( ansadd[fa][i - 1] * dpmul[now][i - 2] % MOD ) % MOD ) * ( ( dpmul[now][i] * qpow( dpadd[now][i] ) % MOD ) * ( ( ansadd[fa][i - 1] - dpadd[now][i - 2] ) * ansadd[now][i] % MOD ) % MOD ) ) % MOD; 

第四个式子实在是太恶心了,一会儿讲,先看总的代码

inline void dfsdown( int now, int fa ){
    for( rint i = 0; i <= k; i++ ){
        dpadd[now][i] = dpmul[now][i] = 1;
    }
    for( rint i = head[now]; i; i = a[i].nxt ){
        int v = a[i].to;
        if( v == fa ) continue ;
        dfsdown( v, now );
        for( rint j = 1; j <= k; j++ ){
            dpadd[now][j] = ( dpadd[now][j] + dpadd[v][j - 1] ) % MOD;
            dpmul[now][j] = ( dpmul[now][j] * dpmul[v][j - 1] ) % MOD;
        }
    }
    for( rint i = 1; i <= k; i++ ) dpmul[now][i] = ( 1ll * dpmul[now][i] * dpadd[now][i] ) % MOD;
}
inline void dfsup( int now, int fa ){
    ansadd[now][0] = ansmul[now][0] = 1;
    ansadd[now][1] = ansmul[now][1] = dpadd[now][1] + dpadd[fa][0]; 
    for( rint i = 2; i <= k; i++ ){
        ansadd[now][i] = ( ansadd[fa][i - 1] + dpadd[now][i] - dpadd[now][i - 2] ) % MOD;
        ansmul[now][i] = ( ( ansmul[fa][i - 1] * qpow( ansadd[fa][i - 1] * dpmul[now][i - 2] % MOD ) % MOD ) * ( ( dpmul[now][i] * qpow( dpadd[now][i] ) % MOD ) * ( ( ansadd[fa][i - 1] - dpadd[now][i - 2] ) * ansadd[now][i] % MOD ) % MOD ) ) % MOD; 
    }
    for( rint i = head[now]; i; i = a[i].nxt ){
        int v = a[i].to;
        if( v == fa ) continue ;
        dfsup( v, now );
    }
}

看一下第四个式子

想把父节点已经确定的信息转移过来,我们得先考虑哪些东西是确定的,哪些是不定的,哪些是我们想知道的

我们先来考虑当前的\(dpmul[now][k]\)\(ansmul[now][k]\)有什么区别,首先没有父节点的信息,其次我们\(dpmul[now][k]\)中乘了\(dpadd[now][k]\),而\(dpadd[now][k]\)是错误的并且我们现在已经算出了正确的\(ansadd[now][k]\),所以我们就要乘\(ansadd[now][k]\),然后除\(dpadd[now][k]\)

注意:因为要取模,所以我们必须求逆元,又因为模数是一个质数,所以我们可以用费马小定理加速

( 啥 事 逆元? )

这样就得到了第一段

( dpmul[now][k] * qpow( dpadd[now][k] ) % MOD ) % MOD * ansadd[now][k] % MOD

注意取模问题,每次出现乘法必取模,不然会玄学得分

这样我们已经得到了一半正确答案,及上图中红色圈的那部分,接着看剩下的那部分

要得到父亲的信息,一定会乘\(ansmul[fa][k - 1]\),然后如上文所说,我们发现\(dpmul[now][k - 2]\)被多乘了一遍,所以要除一下。此时就只有父节点的信息是错误的了,也就是我们\(ansmul[fa][k - 1]\)里的\(fa\)节点的信息是偏大的,因此我们需要得到\(fa\)的真实值,也就是\(ansadd[fa][k - 1] - ansadd[now][k - 2]\),然后再除一下\(ansadd[fa][k - 1]\)( 合并到\(dpmul[now][k - 2]\)一起求逆元 ),这样,我们就得到了第二个式子:

ansmul[now][k] = ( ansmul[fa][k - 1] * qpow( ansadd[fa][k - 1] * dpmul[now][k - 2] % MOD ) % MOD ) * ( ansadd[fa][k - 1] - dpadd[now][k - 2] ) % MOD; 

合并式子就得到了第四式

然后就是一些初始化小问题,我们发现第四式会使用\(k - 2\)的信息,所以我们直接预处理出\(k = 0,1\)的信息,\(k = 0\)时就是1,\(k = 1\)时就是它的儿子数 + 1

最后

一定记得每次乘法都取模,而且写对快速幂!!!!

因为这些我整整写了一天啊QWQ

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!