题解:洛谷 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 的父母会遵守以下两条规则:

  1. 如果 \(A\) 距离 \(C\)\(B\) 距离 \(C\) 近,那么 Chris 的父母先去 Shermie 家寻找 Chris,如果找不到,Chris 的父母再去 Yashiro 家;反之亦然。
  2. 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

【核心思想】

  1. 问题分析:给定一棵带权无向树,固定有三个节点 \(A, B, C\),但教师不知道具体位置。父母从 \(C\) 出发,先去距离 \(C\) 较近的朋友家(若 \(A\)\(B\) 近则先去 \(A\),否则先去 \(B\)),如果找不到再去另一个朋友家。最坏情况下,Chris 在较远的朋友家,此时总路程为 \(dist(C, \text{近}) + dist(\text{近}, \text{远})\)。题目要求在所有可能的 \(A, B, C\) 中,最大化该路程,输出这个最大值。本质上,这是一个树的直径与偏心距的优化问题,可转化为:找出一条直径 \(l \to r\),再找到所有点到直径两端距离较小值的最大值,答案即为直径长度加上该最大值。

  2. 算法选择

    • 两次 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\) 在直径两端附近,最终总路程为直径长度加上该分支点到直径的“最小距离”的最大值。
  3. 关键步骤

    • 读入与建图:读入 \(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\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N)\),三次 DFS 均遍历所有节点和边。
    • 空间复杂度:\(O(N)\),存储邻接表和距离数组。
  5. 树形 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
posted @ 2026-09-01 23:09  团爸讲算法  阅读(10)  评论(0)    收藏  举报