直径 / 重心 DP
过点最长链
本课摘要
直径 / 重心 DP课程回答“过当前节点的多条链如何组合出直径与重心信息”。内容以过点最长链为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断直径 / 重心 DP的适用条件与状态边界
- 围绕“过点最长链”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
树里最长的那条路,怎么一遍找出来
树的直径:任意两点间最长的那条路径。它是许多问题的地基——树网的核、时态同步、乃至距离统计,都要先抓住这条最长路。
最长路可能不经过根,也可能在树的任意角落拐弯。枚举所有点对求最短路再取最大? 起步, 扛不住。 但换个视角就豁然开朗:任何一条路径,都必有一个「最高点」(深度最浅的那个点)。
于是只要枚举这个最高点 ,问题就变成:以 u 为「屋脊」,向它的两个不同孩子方向各挂一条最长的向下链,拼起来就是「过 u 的最长路径」。全局直径 = 所有点的「过点最长」取最大。
状态:向下最长链 down[u]
只需一个状态: = 从 u 出发、一路向下(进入子树)、必含 u 的最长链的长度。它由孩子递推:
取所有孩子里「孩子链 + 连边」最大的那一条。叶子没有孩子,。
而「过 u 的最长路径」要拿两条:在遍历孩子时顺手维护最大 与次大 两条向下链,则
两条链必须来自不同孩子(否则会走回头路),所以取「最大 + 次大」而非「最大 + 最大」。
本质
直径不需要「两遍 BFS」也不需要换根——一遍后序 DFS 就够:每个点在合并孩子的那一瞬间,用「最深 + 次深」结算过它的最长路径。把「枚举最高点」这个观察落实成状态, 塌成 。
跟着算一遍
小树(点权当作到父亲的边权简化演示):根 带 ; 带 ; 带 。设各点权 ,把「过点链」当作点权和:
看直径在哪拐弯
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
同一套合并:最大子树和
把「链」换成「块」,同一套自底向上合并就解另一类题:树上点权有正有负,求一个连通块使权和最大。 状态 = 含 u 的最大子树权和:
盯住那个 :孩子这块若净收益为正就接上,为负就剪断(宁可不要)。答案 = 。 这和直径的「孩子链为正才接」是同一个直觉——只不过直径挑「最深两条」,子树和是「所有正的都要」。
常见陷阱:负权不能一律截断到 0
起手要塞 (即使它是负数),只有孩子的块才用 决定接不接。若把 也钳到非负,全负的树会错报 0。答案初值也要设成 而非 0,防止「必须选至少一个点」时被 0 顶掉。
换个视角看直径。本页从「固定根、一遍 DFS」求出直径与过点最长链;若要对每个点都问「以它为端点的最远距离(偏心距)」,则需要换根 DP 把父方向的信息也回推——那条路线见 E 部分 · 中心 / 偏心距。两条路互补:这里主讲直径本身的推导,换根篇主讲逐点偏心距。
顺带说重心。树的重心是这样一个点:以它为根时,最大的那棵子树节点数最小。 一遍 DFS 求出每点的子树大小 ,判据是——u 的各个方向(每个孩子子树,以及「上方」)都 时,u 即重心。它和直径同属「一遍 DFS 抓全局结构」的固定根树形 DP。
例题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 100005;
struct E { int to, w; };
vector<E> g[N];
long long down[N]; // down[u]:从 u 向下、必含 u 的最长链长
long long ans; // 全局直径
void dfs(int u, int fa)
{
down[u] = 0;
long long best1 = 0, best2 = 0; // 两条最长的孩子向上链
for (E e : g[u])
{
if (e.to == fa) continue;
dfs(e.to, u);
long long chain = down[e.to] + e.w; // 孩子链 + 连它的边
if (chain > best1) { best2 = best1; best1 = chain; }
else if (chain > best2) best2 = chain;
}
down[u] = best1; // 向下最长 = 最深的一条
ans = max(ans, best1 + best2); // ★过 u 的最长 = 两条最深拼接
}
int main()
{
int n;
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b, w;
cin >> a >> b >> w;
g[a].push_back({b, w});
g[b].push_back({a, w});
}
dfs(1, 0);
cout << ans << endl; // 一遍 DFS 即得直径
return 0;
}#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 16005;
vector<int> g[N];
int w[N]; // 点权(可正可负)
int f[N]; // f[u]:含 u 的最大子树权和
int ans;
void dfs(int u, int fa)
{
f[u] = w[u]; // 至少含它自己
for (int v : g[u])
{
if (v == fa) continue;
dfs(v, u);
if (f[v] > 0) f[u] += f[v]; // ★孩子块为正才接上,否则截断
}
ans = max(ans, f[u]);
}
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
cin >> w[i];
for (int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
ans = -0x3f3f3f3f;
dfs(1, 0);
cout << ans << endl;
return 0;
}
