题解:洛谷 P2491 消防
【题目来源】
洛谷:D49 树的直径 P2491 [SDOI2011] 消防_哔哩哔哩_bilibili
【题目描述】
某个国家有 \(n\) 个城市,这 \(n\) 个城市中任意两个都连通且有唯一一条路径,每条连通两个城市的道路的长度为 \(z_i\)。
这个国家的人对火焰有超越宇宙的热情,所以这个国家最兴旺的行业是消防业。由于政\(%\)府对国民的热情忍无可忍(大量的消防经费开销)可是却又无可奈何(总统竞选的国民支持率),所以只能想尽方法提高消防能力。
现在这个国家的经费足以在一条边长度和不超过 \(s\) 的路径(两端都是城市)上建立消防枢纽,为了尽量提高枢纽的利用率,要求其他所有城市到这条路径的距离的最大值最小。
你受命监管这个项目,你当然需要知道应该把枢纽建立在什么位置上。
【输入】
输入包含 \(n\) 行:
第 \(1\) 行,两个正整数 \(n\) 和 \(s\),中间用一个空格隔开。其中 \(n\) 为城市的个数,\(s\) 为路径长度的上界。设结点编号以此为\(1,2,\ldots,n\)。
从第 \(2\) 行到第 \(n\) 行,每行给出 \(3\) 个用空格隔开的非负整数,依次表示每一条边的两个端点编号和长度。例如,2 4 7表示连接结点 \(2\) 与 \(4\) 的边的长度为 \(7\)。
【输出】
输出包含一个非负整数,即所有城市到选择的路径的最大值,当然这个最大值必须是所有方案中最小的。
【输入样例】
5 2
1 2 5
2 3 2
2 4 4
2 5 3
【输出样例】
5
【核心思想】
-
问题分析:给定一棵带正权边的树,需要在树上选择一条长度不超过 \(s\) 的路径(两端均为节点),放置消防枢纽,使得所有节点到这条路径的最大距离最小化。求这个最小化的最大距离。核心在于:最优路径必然位于树的直径上(或与直径密切相关),因此可以先求出直径,再在直径上用双指针枚举可能的路径段,最后考虑直径外分支的影响。
-
算法选择:
- 两次 DFS 求直径:从任意点出发找到最远点,再从此点出发找到直径的另一端点,并记录直径路径上的父节点。
- 双指针枚举直径上的路径:在直径路径上,用两个指针 \(i\) 和 \(j\)(\(j\) 从 \(i\) 开始向前移动)维护一段长度不超过 \(s\) 的子路径,计算该路径两端到直径两端的距离最大值,取最小可能值。
- 分支距离处理:标记直径路径上的节点,对直径上的每个节点,DFS 其非直径分支,计算分支内节点到该路径的最远距离,并更新到最终答案中。
- 答案组成:最终答案为
max(直径两端到所选路径的最大距离, 所有分支到直径路径的最大距离)。
-
关键步骤:
- 建图:读入 \(n, s\) 及 \(n-1\) 条边,构建无向邻接表。
- 求直径:第一次 DFS 从 1 出发找到最远点 \(p\),第二次从 \(p\) 出发找到直径另一端点 \(r\),同时记录每个节点的父节点
pre。 - 双指针枚举直径路径:
- 从直径端点 \(r\) 开始,沿
pre向上遍历节点 \(i\)。 - 对每个 \(i\),令 \(j=i\),然后向前移动 \(j\),直到 \(i\) 到 \(j\) 的路径长度 \(> s\) 时停止(或 \(j\) 到达直径另一端)。
- 计算当前选择路径的代价:
max(dist(i), dist(r)-dist(j)),即路径两端到直径两端的距离的较大值,更新全局答案。
- 从直径端点 \(r\) 开始,沿
- 标记直径路径:从 \(r\) 沿
pre向上标记所有节点为col=1。 - 计算分支距离:对直径上的每个节点,DFS 其非直径分支(跳过
col标记的节点),计算分支内节点到该分支起点(即直径上节点)的最大距离,更新到d数组中。最终答案取max(ans, max(d[i]))。
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),两次 DFS 和双指针遍历均为线性。
- 空间复杂度:\(O(n)\),存储邻接表和辅助数组。
-
核心思想:
- 最优路径在直径上:可以证明,最优路径一定位于树的某一条直径上。因为若路径不在直径上,将其投影到直径上可获得不劣的解。
- 双指针优化:在直径上枚举所有长度不超过 \(s\) 的子路径,通过双指针维护左右端点,\(O(n)\) 内完成。
- 分支影响:分支上的节点到所选路径的距离等于该分支节点到直径上节点的距离,需单独计算并取最大值。
- 适用场景:树上的“最小化最大距离”问题,如消防站选址、应急中心位置等。
【算法标签】
提高+ #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 300005, M = N * 2; // N: 最大节点数, M: 最大边数(无向图双向边)
int n, s; // n: 城市数, s: 路径长度上界
int h[N], e[M], w[M], ne[M], idx; // 链式前向星存图
int pre[N]; // pre[u]: DFS时u的父节点(用于回溯直径路径)
int maxd; // 当前DFS找到的最大距离
int p, r; // p: 临时最远点, r: 直径的一个端点
int ans = 1e9; // 答案:所有城市到选择路径的最大值的最小值
int d[N]; // d[u]: 从某个起点到u的距离
int col[N]; // col[u]: 标记u是否在直径路径上
// 链式前向星:添加无向边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// DFS:求树的直径端点,同时记录父节点信息
// col数组用于限制:只走col标记的点(直径路径)或只走未标记的点(分支)
void dfs(int u, int fa)
{
pre[u] = fa; // 记录u的父节点
// 遍历u的所有邻接点
for (int i = h[u]; i != -1; i = ne[i])
{
int v = e[i];
// 跳过父节点,以及被col限制的点
// 第一次DFS时col全0,走所有点;后续DFS时col标记直径路径,只走非直径分支
if (col[v] || v == fa) continue;
d[v] = d[u] + w[i]; // 更新v到起点的距离
// 更新最远点
if (d[v] > maxd)
{
maxd = d[v];
p = v;
}
dfs(v, u); // 递归DFS
}
}
int main()
{
cin >> n >> s; // 读入城市数和路径长度上界
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入n-1条边
for (int i = 1; i < n; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); // 无向图,双向建边
}
// ========== 第一步:求树的直径 ==========
// 第一次DFS:从任意点1出发,找到最远点p
dfs(1, 0);
d[p] = maxd = 0; // 重置距离数组,准备第二次DFS
// 第二次DFS:从p出发,找到最远点r,同时记录父节点
dfs(p, 0);
r = p; // r是直径的一个端点
// ========== 第二步:在直径路径上用双指针找最优路径 ==========
// 核心思想:选择直径路径上的一段子路径,长度不超过s
// 使得所有城市到这段子路径的最大值最小
// 双指针:i从r向根移动,j维护满足d[j]-d[i]<=s的最远点
// 先假设最优路径在直径上,用双指针枚举
for (int i = r, j = r; i; i = pre[i])
{
// j向前移动,直到i到j的路径长度不超过s
while (j && d[j] - d[i] > s)
j = pre[j];
// 当前选择的路径是直径上从i到j的一段
// d[i]:路径左端点到直径左端点(原始起点p)的距离
// d[r]-d[j]:路径右端点到直径右端点r的距离
// 取两者较大值:即直径两端到选择路径的最大距离
ans = min(ans, max(d[i], d[r] - d[j]));
}
// ========== 第三步:标记直径路径上的所有点 ==========
for (int i = r; i; i = pre[i])
col[i] = 1;
// ========== 第四步:求所有分支到直径路径的最大距离 ==========
// 对于直径路径上的每个点i,DFS其非直径分支,求最远距离
for (int i = r; i; i = pre[i])
{
p = i; // 从i出发
d[i] = 0; // 重置i的距离
// DFS时col标记了直径路径,所以只会走i的非直径分支
dfs(i, pre[i]); // pre[i]作为父节点传入,避免走回直径
}
// 更新答案:取直径两端距离和分支距离的最大值
for (int i = 1; i <= n; i++)
ans = max(ans, d[i]);
// 输出最终答案
cout << ans << endl;
return 0;
}
【运行结果】
5 2
1 2 5
2 3 2
2 4 4
2 5 3
5
浙公网安备 33010602011771号