两次 BFS 求直径,按到两端距离之和等于直径筛出直径结点;每个结点挂到最近的直径结点上,直径上枚举长度不超过 s 的核并用滑动窗口维护窗口内最大挂接深度,偏心距取左露出、右露出与挂深三者的最大值。
OJ: roj
题目 ID: 3559
难度:提高
标签:
创建: 2026-10-02 07:37
更新: 2026-10-10 11:30
形式化题目
设
- 对路径
与结点 ,定义 ; ,即 到全树最远结点的距离,称为 的偏心距。
给定非负整数
正解
思路
第一步:把问题收缩到直径上。 设某条直径为
- 树上任意一点到
的距离,不超过它到 所在直径上更远一端的距离。具体地:位于 左侧(朝 一侧)的结点到 的距离 核左端到 的距离,记 ;右侧同理记 ; - 于是
。取等号的最坏情形可以在直径端点处取到,所以直径上长度为 的整段核本身就是一个可行解,最优值不会小于从直径上选出候选核后按该式算出的最小值。
因此只需在这条直径上枚举候选核即可。
第二步:求直径与直径结点。 经典两次 BFS:在树上,离任意一点最远的结点必是某条直径的端点。先从结点
第三步:筛出直径结点。 结点
(必要:直径路径上按定义成立;充分:若
第四步:把每个结点"挂"到直径上。 结点
的挂点: ,使 最小(可证这样的 唯一——若最小值在相邻两处打平,会拼出比直径更长的路径); 的挂接深度: 。
窗口内所有挂点的深度取
其中
第五步:滑窗枚举。 枚举左端
样例 2 的直径与核
这张图展示样例 2 中两次 BFS 找到的直径,黄色结点按位置排成直径:
graph LR
1 ==>|2| 3
2 ---|2| 3
3 ==>|6| 4
4 ---|3| 5
4 ---|4| 6
4 ==>|2| 7
7 ==>|3| 8
style 8 fill:#ffe08a
style 7 fill:#ffe08a
style 4 fill:#ffe08a
style 3 fill:#ffe08a
style 1 fill:#ffe08a
直径端点是 8 与 1,直径长度为 13;黄色结点 [8, 7, 4, 3, 1] 依次位于直径的位置 [0, 3, 5, 11, 13]。观察发现:非黄色结点 2、5、6 的路径都先经过某个黄色结点(它们的挂点),偏心距只需关心这些挂点的深度,以及核两端露出的直径长度。
| 结点 v | 挂点 path[t] | 位置 t | 挂接深度 |
|---|---|---|---|
| 2 | 3 | 3 | 2 |
| 5 | 4 | 2 | 3 |
| 6 | 4 | 2 | 4 |
这张表只列出非直径结点的挂接情况:挂接深度就是该结点到挂点的距离。当 s = 6 时,最优核是结点 4 到 3 这一段,偏心距为 4 到直径端点 8 的距离,右露出为核右端 3 到端点 1 的距离,窗口内最大挂深来自结点 6)。
代码
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, s;
vector<pair<int, int> > adj[305]; // (v, w)
int dist_from[305][305]; // dist_from[v-1][x] = x 到 v 的距离
int u0_, uk_;
// 树中离 src 最远的结点编号 + 各点到 src 的距离
void farthest(int src, int dist[]) {
for (int i = 1; i <= n; i++) dist[i] = -1;
dist[src] = 0;
deque<int> q;
q.push_back(src);
while (!q.empty()) {
int u = q.front(); q.pop_front();
for (size_t i = 0; i < adj[u].size(); i++) {
int v = adj[u][i].first, w = adj[u][i].second;
if (dist[v] < 0) { dist[v] = dist[u] + w; q.push_back(v); }
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> s;
for (int i = 0; i < n - 1; i++) {
int a, b, w;
cin >> a >> b >> w;
adj[a].push_back(make_pair(b, w));
adj[b].push_back(make_pair(a, w));
}
// 两次 BFS 定位直径
farthest(1, dist_from[0]);
int far1 = 1;
for (int i = 2; i <= n; i++)
if (dist_from[0][i] > dist_from[0][far1]) far1 = i;
u0_ = far1;
farthest(u0_, dist_from[0]); // dist0
int far2 = u0_;
for (int i = 1; i <= n; i++)
if (dist_from[0][i] > dist_from[0][far2]) far2 = i;
uk_ = far2;
int dist0[305], distK[305];
for (int i = 1; i <= n; i++) dist0[i] = dist_from[0][i];
farthest(uk_, distK);
int diameter = dist0[uk_];
// 直径上的结点,按位置排序
vector<int> path;
for (int x = 1; x <= n; x++)
if (dist0[x] + distK[x] == diameter) path.push_back(x);
sort(path.begin(), path.end(), [&](int a, int b) { return dist0[a] < dist0[b]; });
int k = (int)path.size();
vector<int> pos(k);
for (int t = 0; t < k; t++) pos[t] = dist0[path[t]];
// 全源距离
for (int v = 1; v <= n; v++) farthest(v, dist_from[v - 1]);
// depAt[t]:以 path[t] 为挂点的结点的最大距离
vector<int> depAt(k, 0);
for (int v = 1; v <= n; v++) {
int near = 0;
int best = 1 << 30;
for (int t = 0; t < k; t++) {
int dd = dist_from[v - 1][path[t]];
if (dd < best) { best = dd; near = t; }
}
depAt[near] = max(depAt[near], dist_from[v - 1][path[near]]);
}
// 枚举候选核 [i..j]
int ans = diameter;
int j = 0;
for (int i = 0; i < k; i++) {
if (j < i) j = i;
while (j + 1 < k && pos[j + 1] - pos[i] <= s) j++;
int cover = 0;
for (int t = i; t <= j; t++) cover = max(cover, depAt[t]);
int left_tail = pos[i];
int right_tail = diameter - pos[j];
ans = min(ans, max(cover, max(left_tail, right_tail)));
}
cout << ans << "\n";
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-07-05 21:47
# update_at: 2026-07-05 21:47
import sys
from collections import deque
def farthest(adj: list[list[tuple[int, int]]], src: int) -> tuple[int, list[int]]:
"""返回树中离 src 最远的结点编号,以及各结点到 src 的距离(树上路径唯一,BFS 即最短路)。"""
dist = [-1] * len(adj)
dist[src] = 0
dq = deque([src])
while dq:
u = dq.popleft()
for v, w in adj[u]:
if dist[v] < 0:
dist[v] = dist[u] + w
dq.append(v)
far = max(range(1, len(adj)), key=dist.__getitem__)
return far, dist
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, s = next(data), next(data)
adj: list[list[tuple[int, int]]] = [[] for _ in range(n + 1)]
for _ in range(n - 1):
a, b, w = next(data), next(data), next(data)
adj[a].append((b, w))
adj[b].append((a, w))
# 两次 BFS 定位直径:树上离任一点最远的点必是某条直径的端点
u0, _ = farthest(adj, 1)
uk, dist0 = farthest(adj, u0)
_, distK = farthest(adj, uk)
diameter = dist0[uk]
# 满足 d(u0,x)+d(x,uk)=diameter 的 x 恰是直径上的结点,按位置排序
path = sorted((x for x in range(1, n + 1) if dist0[x] + distK[x] == diameter), key=dist0.__getitem__)
pos = [dist0[x] for x in path] # pos[t]:直径第 t 个结点在直径上的位置
k = len(path)
# n ≤ 300,直接对每个结点做一次 BFS 得全源距离
dist_all = [farthest(adj, v)[1] for v in range(1, n + 1)]
# depAt[t]:以 path[t] 为"挂点"(到直径路径最近的直径结点)的结点的最大距离
# 结点 v 的路径与直径交于唯一点,argmin 唯一给出挂点,最小值即挂接深度
depAt = [0] * k
for v in range(1, n + 1):
near = min(range(k), key=lambda t: dist_all[v - 1][path[t]])
depAt[near] = max(depAt[near], dist_all[v - 1][path[near]])
# 枚举直径上的候选核 [i..j](长度 ≤ s),双指针保证 j 只增不减
# 窗口外两侧结点的距离被 pos[i]、diameter-pos[j] 支配(可证 挂深 ≤ 挂点到较远端点的长)
ans = diameter
j = 0
for i in range(k):
if j < i:
j = i
while j + 1 < k and pos[j + 1] - pos[i] <= s:
j += 1
cover = max(depAt[i : j + 1]) # 窗口内挂点深度
left_tail = pos[i] # 直径左端到核左端的距离
right_tail = diameter - pos[j] # 直径右端到核右端的距离
ans = min(ans, max(cover, left_tail, right_tail))
print(ans)
if __name__ == "__main__":
solve()复杂度
- 时间:对每个结点做一次 BFS 为
( ,全源距离表约 项);双指针扫直径为 ,总复杂度 ,与标准 C++ 解法同阶。 - 空间:全源距离表
。
总结
本题是经典的"树的直径 + 核"模型,核心链条是三次收缩:
- 求直径:两次 BFS 定端点,用
筛出直径结点; - 挂点分解:每个结点到核的路径必先经过直径上离它最近的结点,把"全树到核的最远距离"拆成"左右露出的直径长度"与"窗口内最大挂接深度"两个可局部维护的量;
- 滑窗枚举:候选核的左右端点在直径上都是单调的,双指针加窗口最大值即可。
