1 条题解
-
0
思路与算法
任意固定一条直径可能漏掉其它同长度路径上的点,因此需要判断每个点是否属于至少一条直径。
以0为根,迭代得到父亲和遍历顺序,再逆序求每点来自孩子方向的最长、次长分支best、second。所有best[u]+second[u]的最大值就是直径长度D。
正序传递父侧分支up:对u的孩子v,先从u的孩子分支中排除v,再与up[u]比较取较大者,加1得到up[v]。排除时,若v提供了best[u],改用second[u];否则仍用best[u]。
对每点,把父侧up与两条孩子分支放在一起取前两大值,若其和等于D,就输出该点。按编号0..n-1检查,自然得到升序答案。代码用边数度量长度,比题面路径点数少1,不改变最长路径的集合。
正确性说明
逆序处理保证每个孩子向下最远距离已知,因此best和second准确表示两条不同孩子方向的最长分支。任意树路径在其最高点处分成至多两条向下分支,故全树这些和的最大值是D。
从v经过父亲u的路径,除去边v-u后只能走u的父侧或其它孩子分支;排除v后取最大值并加1,恰得到v的父侧最远距离。归纳可知全部up值正确。
任何经过u的简单路径最多使用u两侧不同方向的分支,前两大分支之和正是经过u的最长路径。它等于D当且仅当存在一条全树最长路径经过u,因此逐点判定得到所有直径顶点的并集。
复杂度
n点。邻接表与全部数组的初始化O(n),正逆遍历、父侧传递及输出判定均只访问每条边常数次,总时间O(n),空间O(n)。遍历采用显式顺序数组,不依赖深递归。
易错点
最长分支有并列时,次长也可能等于最长,必须保留两条不同孩子方向的贡献。排除当前孩子时不能直接使用包含它的最大值。单点树的直径长度为0,唯一节点0也应输出。题目只要节点编号列表,不要额外输出直径长度或答案个数。
参考代码(C++11)
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<vector<int>> g(n); for (int i = 1, u, v; i < n; ++i) { cin >> u >> v; g[u].push_back(v); g[v].push_back(u); } vector<int> parent(n, -1), order(1, 0), best(n), second(n), up(n); parent[0] = 0; for (int i = 0; i < n; ++i) for (int v : g[order[i]]) if (v != parent[order[i]]) parent[v] = order[i], order.push_back(v); int diameter = 0; for (int i = n - 1; i >= 0; --i) { int u = order[i]; for (int v : g[u]) if (parent[v] == u) { int d = best[v] + 1; if (d > best[u]) second[u] = best[u], best[u] = d; else if (d > second[u]) second[u] = d; } diameter = max(diameter, best[u] + second[u]); } for (int u : order) for (int v : g[u]) if (parent[v] == u) { int other = best[v] + 1 == best[u] ? second[u] : best[u]; up[v] = max(up[u], other) + 1; } for (int u = 0; u < n; ++u) { int a = best[u], b = second[u]; if (up[u] > a) b = a, a = up[u]; else b = max(b, up[u]); if (a + b == diameter) cout << u << '\n'; } }
- 1