树形DP-换根DP-简记

导入

在普通树形DPDPDP中,一般就使用一个DPDPDP数组,代表以i为根的子树中的最值,但是对于某些题目中一个数组已经不够用,此时该加入什么,以及转移方程是什么,用一个题目来说明。

题目:P3478

给定一个 nnn 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

按照普通 dpdpdp 来考虑,dpdpdp 数组状态为以i为根的子树中的深度之和,用 sumsumsum 数组记录i以i为根的子树中的节点数,如果现在在 xxx,yyyxxx 的孩子,那么转移就是。

dpx=Σdpy+sumy dp_x = \Sigma {dp_y+sum_y} dpx=Σdpy+sumy

即(y中)每个点都会往后再走一步走到x,所以要加sumysum_ysumy

可是这样就结束了吗?并没有。

因为当i变为整个树的根时,它的孩子还是它的孩子,但是它的父亲也变成孩子了。

所以还要用一个数组( dp2dp2dp2 ),储存除了以 iii 为根的子树的的深度之和。

那么转移就是它父亲向上的+它父亲向下的-它那支对父亲的影响+此点到父亲那些。

如图:

在这里插入图片描述

转移:

dp2x=dp2fa+dpfa−(dpx+sumx)+(sum[1]−sum[x]) dp2_x = dp2_{fa} + dp_{fa} - (dp_x+sum_x) + (sum[1]-sum[x]) dp2x=dp2fa+dpfa(dpx+sumx)+(sum[1]sum[x])

然后这边给出代码:

//P2986来的
#include <bits/stdc++.h>
using namespace std;
#define int long long
int sum[1000005];//i的子树中一共有几头牛(包括i)
int down[1000005];//i的子树中那些牛来到这不方便值
int up[1000005];//除了i的子树中那些,其他点来这的不方便值
    int n;
vector <int> a[1000005];
int cnt[1000005];
void dfs(int x,int f){
    sum[x] = 1;
    for (int i = 0; i < a[x].size(); i++){
        int y = a[x][i];
        if (y != f){
            dfs(y,x);
            sum[x] += sum[y];
            down[x] += down[y]+sum[y];
        }
    }
    //cout << down[x]<<" " << x << " \n";
}

void dfs2(int x,int f){
    if (f!=0){
       up[x] = up[f]+down[f]-(down[x]+sum[x])+(sum[1]-sum[x]);//注意sum[1]而不是n...
        //+(sum[1]-sum[x])同上
        //up[f]+down[f]-(down[x]+sum[x]),除了我这条枝干到f外所有点到f的不方便程度
        //注意+sum[x]!
    }
    for (int i = 0; i < a[x].size(); i++){
        int y = a[x][i];
        if (y != f) dfs2(y,x);
    }
}
signed main(){
    ios::sync_with_stdio(false);  // 关闭 C 和 C++ 输入输出同步
    cin.tie(nullptr);         
    cout.tie(nullptr);   
    cin >> n;
    for (int i = 1; i <= n-1; i++){
        int u,v;
        cin >>u >> v;
        a[u].push_back(v);
        a[v].push_back(u);
    }
    dfs(1,0);
    dfs2(1,0);
    int max_ = -114514,id;
    for (int i = 1; i <= n; i++){
        if (max_ < down[i]+up[i]){
            max_ = down[i]+up[i];
            id = i;
        }
        //cout << up[i] << " " << down[i] << "\n";
    }
    cout << id;
    return 0;
}

题目:P9437

这里是引用

轻松得出转移,sumsumsum 代表节点数:

dpx=Σdpy∗10len(ax)+sumy∗ax dp_x = \Sigma dp_y*10^{len(a_x)}+sum_y*a_x dpx=Σdpy10len(ax)+sumyax

lenlenlen 函数为这个数是几位数。

这转移的解释:

加上 yyy 的那些延升到 xxx 的路(即 dpydp_ydpy 每一项往大移一下(∗10len(ax)*10^{len(a_x)}10len(ax),提取公因数,从 dpydp_ydpy 每一项提出来),每一项再加入axa_xax (sumy∗axsum_y*a_xsumyax))

dp2dp2dp2怎么转移?

依旧参考这个图。

在这里插入图片描述
可以得出:

父亲向上的:dp2fadp2_{fa}dp2fa

它父亲向下的:dpfadp_{fa}dpfa

它那支对父亲的影响:(dpx∗10len(w[fa])+sumx∗afa)(dp_x*10^{len(w[fa])}+sum_{x}*a_{fa})(dpx10len(w[fa])+sumxafa)

其实这里直接抄 dpdpdp 的转移就行。

定义 ans=dp2fa+dpfa−(dpx∗10len(a[fa])+sumx∗afa) ans = dp2_{fa}+dp_{fa}-(dp_x*10^{len(a[fa])}+sum_{x}*a_{fa})ans=dp2fa+dpfa(dpx10len(a[fa])+sumxafa)

那此点到父亲那些的贡献就是ans∗10len(a[x])+sum2x∗axans * 10^{len(a[x])}+sum2_x*a_xans10len(a[x])+sum2xax

sum2sum2sum2 代表 ansansans 里有几项,这个贡献跟 dpdpdp 的转移差不多,不再过多赘述了。

sum2sum2sum2的更新:

依旧按照那个图来

sumx=sumfa+sum2fa−sumxsum_x = sum_{fa} + sum2_{fa}-sum_xsumx=sumfa+sum2fasumx

这边可以得出代码:

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int mod = 998244353;//变量名终于不写boeing757了!
int dp[1000004];//以i为起点的(考虑i的树)
int dp2[1000004];//以i为结尾的(考虑非i的树)
int sum[1000004];//有几条路线(考虑i的树)
int sum2[1000004];//有几条路线(考虑非i的树)
int w[1000004];
int ten[1000004];
vector <int> a[1000004];
int len(int x){
    if(x == 0) return 1;
    int cnt = 0;
    while(x) cnt++, x /= 10;
    //peepsick说不用flag。。。
    return cnt;
}
void dfs(int x,int fa){
    //自己到自己
    dp[x] = w[x];
    sum[x] ++;//自己到自己是一条
    for (int i = 0; i < a[x].size(); i++){
        int y = a[x][i];
        if (y != fa){
            dfs(y,x);
            sum[x] += sum[y];
            dp[x] += dp[y]%mod*ten[len(w[x])]%mod+sum[y]%mod*w[x]%mod;//加上y的那些延升到x的路(即dp[y]每一项往大移一下(*ten[len(w[x])],提取公因数,从dp[y]每一项提出来),每一项再加入w[x](sum[y]*w[x]))
        }
    }
}
void dfs2(int x,int fa){
    if (fa != 0){
        sum2[x] = sum[fa] + sum2[fa]-sum[x];
        int self = dp[x]%mod*ten[len(w[fa])]%mod+sum[x]%mod*w[fa]%mod;//减去自己那支(直接抄dp的转移)
        int ans = (dp2[fa]%mod+dp[fa]%mod-self+mod) %mod;
        dp2[x] = ((ans%mod * ten[len(w[x])]%mod)%mod+sum2[x]%mod*w[x]%mod)%mod;//,注意加上w[x]并*位数
    }
    for (int i = 0; i < a[x].size(); i++){
        int y = a[x][i];
        if (y != fa) dfs2(y,x);
    }
}
signed main(){
    ten[0] = 1;
    for (int i = 1; i<= 10; i++) ten[i] = ten[i-1] * 10%mod;
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> w[i];
    for (int i = 1; i <= n-1; i++){
        int u;
        cin >> u;
        a[u].push_back(i+1);
        a[i+1].push_back(u);
    }
    dfs(1,0);
    dfs2(1,0);
    int ans = 0;
    for (int i = 1; i <= n; i++){
        (ans += dp[i]%mod+dp2[i]%mod)%=mod;
    }
    cout << ans;
    return 0;
}

最后给出一些换根的题目

在这里插入图片描述

第一题为模板。

333 道题较为简单。

后面考察一些思考,以及代码实现有细节(至少对我来说)。

完结!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值