题解:洛谷 P4408 逃学的小孩
【题目来源】
洛谷:P4408 [NOI2003] 逃学的小孩 / 数据生成器 - 洛谷
【题目描述】
Chris 家的电话铃响起了,里面传出了 Chris 的老师焦急的声音:“喂,是 Chris 的家长吗?你们的孩子又没来上课,不想参加考试了吗?”一听说要考试,Chris 的父母就心急如焚,他们决定在尽量短的时间内找到 Chris。他们告诉 Chris 的老师:“根据以往的经验,Chris 现在必然躲在朋友 Shermie 或 Yashiro 家里偷玩《拳皇》游戏。现在,我们就从家出发去找 Chris,一旦找到,我们立刻给您打电话。”说完砰的一声把电话挂了。
Chris 居住的城市由 \(N\) 个居住点和若干条连接居住点的双向街道组成,经过街道 \(x\) 需花费 \(T_{x}\) 分钟。可以保证,任意两个居住点间有且仅有一条通路。Chris 家在点 \(C\),Shermie 和 Yashiro 分别住在点 \(A\) 和点 \(B\)。Chris 的老师和 Chris 的父母都有城市地图,但 Chris 的父母知道点 \(A\)、\(B\)、\(C\) 的具体位置而 Chris 的老师不知。
为了尽快找到 Chris,Chris 的父母会遵守以下两条规则:
- 如果 \(A\) 距离 \(C\) 比 \(B\) 距离 \(C\) 近,那么 Chris 的父母先去 Shermie 家寻找 Chris,如果找不到,Chris 的父母再去 Yashiro 家;反之亦然。
- Chris 的父母总沿着两点间唯一的通路行走。
显然,Chris 的老师知道 Chris 的父母在寻找 Chris 的过程中会遵守以上两条规则,但由于他并不知道 \(A\)、\(B\)、\(C\) 的具体位置,所以现在他希望你告诉他,最坏情况下 Chris 的父母要耗费多长时间才能找到 Chris?
【输入】
输入文件第一行是两个整数 \(N\) 和 \(M\),分别表示居住点总数和街道总数。
以下 \(M\) 行,每行给出一条街道的信息。第 \(i+1\) 行包含整数 \(U_{i}\)、\(V_{i}\)、\(T_{i}\),表示街道 \(i\) 连接居住点 \(U_{i}\) 和 \(V_{i}\),并且经过街道 \(i\) 需花费 \(T_{i}\) 分钟。街道信息不会重复给出。
【输出】
输出文件仅包含整数 \(T\),即最坏情况下 Chris 的父母需要花费 \(T\) 分钟才能找到 Chris。
【输入样例】
4 3
1 2 1
2 3 1
3 4 1
【输出样例】
4
【核心思想】
-
问题分析:给定一棵带权无向树,固定有三个节点 \(A, B, C\),但教师不知道具体位置。父母从 \(C\) 出发,先去距离 \(C\) 较近的朋友家(若 \(A\) 比 \(B\) 近则先去 \(A\),否则先去 \(B\)),如果找不到再去另一个朋友家。最坏情况下,Chris 在较远的朋友家,此时总路程为 \(dist(C, \text{近}) + dist(\text{近}, \text{远})\)。题目要求在所有可能的 \(A, B, C\) 中,最大化该路程,输出这个最大值。本质上,这是一个树的直径与偏心距的优化问题,可转化为:找出一条直径 \(l \to r\),再找到所有点到直径两端距离较小值的最大值,答案即为直径长度加上该最大值。
-
算法选择:
- 两次 DFS 求树的直径:第一次从任意点出发找到最远点 \(l\),第二次从 \(l\) 出发找到最远点 \(r\),得到直径长度 \(D\) 及直径端点。
- 第三次 DFS 求距离:从 \(r\) 出发,记录每个点到 \(r\) 的距离,同时保留第一次从 \(l\) 出发的距离,得到每个点到两端点的距离。
- 计算答案:令 \(ecc[u] = \min(dist(u,l), dist(u,r))\),取 \(E = \max_{u} ecc[u]\),则答案 \(= D + E\)。
- 正确性:在树上,最坏情况对应选取 \(C\) 为某个分支点,\(A\) 和 \(B\) 在直径两端附近,最终总路程为直径长度加上该分支点到直径的“最小距离”的最大值。
-
关键步骤:
- 读入与建图:读入 \(N, M\)(\(M = N-1\))及每条边的权值,构建无向邻接表。
- 第一次 DFS:从节点 \(1\) 出发,记录每个节点的距离
d[u],找到最远点l。 - 第二次 DFS:从
l出发,记录距离到dd[u],找到最远点r,直径长度 \(D = dd[r]\),同时记录父节点(用于回溯,但本题不需要)。 - 第三次 DFS:从
r出发,记录距离到d[u]。 - 计算 \(E\):遍历所有节点,计算
min(d[u], dd[u]),取其最大值。 - 输出答案:\(D + E\)。
-
时间/空间复杂度:
- 时间复杂度:\(O(N)\),三次 DFS 均遍历所有节点和边。
- 空间复杂度:\(O(N)\),存储邻接表和距离数组。
-
树形 DP + 直径的核心思想:
- 问题转化:最坏情况下的总路程可分解为“直径长度”与“从某个点到直径的垂直距离”之和,其中垂直距离定义为该点到直径两端点距离的较小值。
- 三次 DFS 的必要性:通过分别从直径两端出发,得到每个点到两端点的距离,从而计算该点到直径的“最近距离”。
- 贪心最优性:最优的 \(A, B\) 必然位于直径两端,\(C\) 位于某个使垂直距离最大的分支点,因此答案可由此计算。
- 适用于树上“最坏路径”类问题,结合直径和偏心距的求解。
【算法标签】
提高+ #树形DP
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将 int 定义为 long long,防止距离累加溢出
const int N = 200005, M = N * 2; // N: 最大居住点数, M: 最大边数(无向图双向边)
int n, m; // n: 居住点总数, m: 街道总数
int h[N], e[M], w[M], ne[M], idx; // 链式前向星存图
int l, r, p, maxd; // l: 直径左端点, r: 直径右端点, p: 临时最远点, maxd: 当前最大距离
int d[N]; // d[u]: 从当前起点到u的距离(第二次DFS用)
int dd[N]; // dd[u]: 从直径左端点l到u的距离(第一次从l出发的DFS结果)
int pre[N]; // pre[u]: DFS时u的父节点
int d1, d2; // d1: 树的直径长度, d2: 所有点到直径两端距离的最小值中的最大值
// 链式前向星:添加无向边
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// DFS:求树的直径端点,同时记录父节点信息
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];
if (v == fa) continue; // 跳过父节点
d[v] = d[u] + w[i]; // 更新v到起点的距离
// 更新最远点
if (d[v] > maxd)
{
maxd = d[v];
p = v;
}
dfs(v, u); // 递归DFS
}
}
signed main() // 使用 signed 替代 int,因为 #define int long long
{
cin >> n >> m; // 读入居住点总数和街道总数
// 初始化链式前向星
memset(h, -1, sizeof(h));
// 读入m条街道
while (m--)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c); // 无向图,双向建边
}
// ========== 第一步:求树的直径 d1 ==========
// 第一次DFS:从任意点1出发,找到最远点p
dfs(1, 0);
l = p; // l是直径的一个端点
d[l] = maxd = 0; // 重置距离数组,准备第二次DFS
// 第二次DFS:从l出发,找到最远点r,同时记录父节点
dfs(l, 0);
r = p; // r是直径的另一个端点
d1 = d[r]; // d1为树的直径长度(l到r的距离)
// 保存从l出发的距离数组到dd
memcpy(dd, d, sizeof(d));
// 第三次DFS:从r出发,求每个点到r的距离
d[r] = maxd = 0; // 重置距离数组
dfs(r, 0);
// ========== 第二步:计算最坏情况下的时间 ==========
// 核心思想:
// 设C为Chris家,A和B为两个朋友家。
// 父母先去较近的朋友家,如果找不到再去较远的。
// 最坏情况:Chris在较远的朋友家,父母需要走:C到近处 + 近处到远处 = 2*C到近处 + 近处到远处?
// 实际上,最坏情况对应树的直径上的某个分布。
// 对于每个点i,min(d[i], dd[i]) 表示该点到直径两端距离的最小值
// 这相当于该点作为C(Chris家)时,到较近端点的距离
// d2取所有点中这个最小值的最大值(最坏情况下的最近端点距离)
for (int i = 1; i <= n; i++)
d2 = max(d2, min(d[i], dd[i]));
// 最坏情况下的总时间 = 直径长度 + 所有点到直径两端距离最小值的最大值
// 解释:父母最坏情况下需要走遍直径,再加上从某个分支点到直径的往返
cout << d1 + d2 << endl;
return 0;
}
【运行结果】
4 3
1 2 1
2 3 1
3 4 1
4
浙公网安备 33010602011771号