洛谷 P10962:Computer ← 换根DP

【题目来源】
https://www.luogu.com.cn/problem/P10962
http://acm.hdu.edu.cn/showproblem.php?pid=2196

【题目描述】
某学校在一段时间前购买了第一台计算机(因此这台计算机的编号是 1)。在最近几年中,学校又购买了 N-1 台新计算机。每台新计算机都连接到之前已经安装的计算机之一。学校的管理人员对网络运行缓慢感到担忧,想知道每台计算机需要发送信号的最大距离 Si(即到最远计算机的电缆长度)。你需要提供这些信息。

P10962

提示:示例输入对应于此图。从图中可以看到,计算机 4 是距离计算机 1 最远的,因此 S1=3。计算机 4 和 5 是距离计算机 2 最远的,因此 S2=2。计算机 5 是距离计算机 3 最远的,因此 S3=3。我们还得到 S4=4,S5=4。

【输入格式】
输入文件包含多个测试用例。每个用例的第一行是自然数 N(N≤10000),接下来的 N-1 行描述了计算机的连接情况。第 i 行(i≥2)包含两个自然数 --- 第 i 台计算机连接的计算机编号和用于连接的电缆长度。电缆的总长度不超过 10^9。输入行中的数字由空格分隔。

【输出格式】
对于每个用例,输出 N 行。第 i 行必须包含第 i 台计算机的数值 Si(1≤i≤N)。

【输入样例】
5
1 1
2 1
3 1
1 1​​​​​​​

【输出样例】
3
2
3
4
4

【数据范围】
N≤10000,1≤i≤N

【算法分析】
● 换根 DP 是树形 DP 的一种重要技术,用于解决需要以树中‌不同节点为根‌分别计算答案的问题。其核心思想是在一次动态规划后,通过‌推导出换根时的状态转移公式‌,高效地计算出所有节点作为根时的结果,避免对每个根节点都进行一次 O(n) 的树形DP(那样总复杂度为 O(n²))。

● 对于大多数简单的树形 DP 问题,如计算子树大小、节点深度、简单路径统计等,算法通常对每个节点执行常数次操作,因此时间复杂度为 ‌O(n)‌。

● 换根 DP 通常遵循一个固定的两遍 DFS 流程:
(一)第一次 DFS(固定根,收集信息)‌
(1)任选一个节点(通常为 1 号节点)作为初始根。
(2)进行一次自底向上的树形 DP,计算以该节点为根时,各子树的状态信息。这些信息通常包括:子树大小(siz[u])、子树内节点到根的距离和(dis[u])、或其他与问题相关的子树最优解。
(二)第二次 DFS(换根,推导全局)‌
(1)这是算法的关键。基于第一次 DFS 得到的信息,从初始根节点开始,进行第二次 DFS。
(2)在遍历过程中,当从父节点 u 走向子节点 v 时,利用已知的以 u 为根时的全局答案 dp[u],推导出以 v 为根时的全局答案 dp[v]。
(3)这个推导过程就是 ‌“换根公式”‌,它分析了当根从 u 移到 v 时,哪些节点的贡献发生了变化(例如,深度增加或减少),并据此更新答案。

● 经典示例:所有节点到其他节点距离之和
​​​​​​​代码详见:https://blog.csdn.net/hnjzsyjyj/article/details/157169859
这是一个最经典的换根DP问题,清晰地展示了换根公式的推导过程。
(1)问题‌:给一棵树,求以每个节点为根时,所有节点到该根节点的深度之和。
(2)定义‌:siz[u]:以 u 为根的子树中的节点数、dis[u]:以 u 为根时,所有节点到 u 的深度之和。

P3478_1

(3)步骤‌:
第一次 DFS‌:以节点 1 为根,计算 siz[u] 和初始的 dis[1]。
换根公式推导‌:当根从 u 换到其子节点 v 时,所有在 v 子树中的节点,到新根 v 的距离比到旧根 u 的距离‌减少1‌。这部分贡献总共减少 siz[v]。所有不在 v 子树中的节点(共 n - siz[v] 个),到新根 v 的距离比到旧根 u 的距离‌增加1‌。这部分贡献总共增加 n - siz[v]。
因此,dis[v] = dis[u] - siz[v] + (n - siz[v]) = dis[u] + n - 2 * siz[v]。
第二次 DFS‌:应用此公式,即可从 dp[1] 递推计算出所有节点的 dis[i]。

● 本题数据量达到了 10^4,可以在代码中加入如下语句(https://blog.csdn.net/hnjzsyjyj/article/details/143176072),避免 TLE。

ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);

当然,也可以使用“快读(https://blog.csdn.net/hnjzsyjyj/article/details/120131534)”函数,代码如下。

int read() { //fast read
    int x=0,f=1;
    char c=getchar();
    while(c<'0' || c>'9') { //!isdigit(c)
        if(c=='-') f=-1;
        c=getchar();
    }
    while(c>='0' && c<='9') { //isdigit(c)
        x=x*10+c-'0';
        c=getchar();
    }
    return x*f;
}

● 本题输入样例解释
第 1 行,5,树的总节点数 n=5(节点编号为 1~5)。
第 2 行,1 1,对应节点 2,它的父节点是 1,与父节点之间的边权为 1。
第 3 行,2 1,对应节点 3,它的父节点是 2,与父节点之间的边权为 1。
第 4 行,3 1,对应节点 4,它的父节点是 3,与父节点之间的边权为 1。
第 5 行,1 1,对应节点 5,它的父节点是 1,与父节点之间的边权为 1。​​​​​​​

【算法代码】

#include <bits/stdc++.h>
using namespace std;

const int N=1e4+5;
int f[N]; //Local (subtree) longest
int g[N]; //Local (subtree) secondary length
int dis[N]; //Globally (tree) longest
int val[N<<1],e[N<<1],ne[N<<1],h[N],idx;
int n;

void add(int a,int b,int w) {
    val[idx]=w,e[idx]=b,ne[idx]=h[a],h[a]=idx++;
}

void dfs1(int u) {
    for(int i=h[u]; i!=-1; i=ne[i]) {
        int j=e[i],v=val[i];
        dfs1(j);
        if(f[j]+v>f[u]) g[u]=f[u],f[u]=f[j]+v;
        else if(f[j]+v>g[u]) g[u]=f[j]+v;
    }
}

void dfs2(int u,int t) {
    dis[u]=max(t,f[u]);
    int fir=max(t,f[u]),sec=max(t,g[u]);
    for(int i=h[u]; i!=-1; i=ne[i]) {
        int j=e[i],v=val[i];
        if(f[j]+v==f[u]) dfs2(j,sec+v);
        else dfs2(j,fir+v);
    }
}

int main() {
    while(cin>>n) {
        memset(h,-1,sizeof h);
        idx=0;
        for(int i=1; i<=n; i++) f[i]=g[i]=0;
        for(int i=2; i<=n; i++) {
            int x,w;
            cin>>x>>w;
            add(x,i,w);
        }

        dfs1(1);
        dfs2(1,0);

        for(int i=1; i<=n; i++) {
            cout<<dis[i]<<endl;
        }
    }
    return 0;
}

/*
in:
5
1 1
2 1
3 1
1 1

out:
3
2
3
4
4
*/





【参考文献】
https://mp.weixin.qq.com/s/Ulg9GIl0u2Qgh45FnC9OMg
https://blog.csdn.net/hnjzsyjyj/article/details/157169859
https://blog.csdn.net/hnjzsyjyj/article/details/157134513
https://www.cnblogs.com/mingloch/p/17678247.html
https://www.luogu.com.cn/problem/solution/P10962



 

posted @ 2026-01-20 21:07  Triwa  阅读(1)  评论(0)    收藏  举报