AcWing 1072. 树的最长路径

\(AcWing\) \(1072\) 树的最长路径

一、题目描述

给定一棵树,树中包含 \(n\) 个结点(编号\(1\)~\(n\))和 \(n−1\) 条无向边,每条边都有一个权值。

现在请你找到树中的一条最长路径。

换句话说,要找到一条路径,使得使得路径两端的点的距离最远。

注意:路径中可以只包含一个点。

输入格式
第一行包含整数 \(n\)

接下来 \(n−1\) 行,每行包含三个整数 \(a_i,b_i,c_i\),表示点 \(a_i\)\(b_i\) 之间存在一条权值为 \(c_i\) 的边。

输出格式
输出一个整数,表示树的最长路径的长度。

数据范围
\(1≤n≤10000,\)
\(1≤a_i,b_i≤n,\)
\(−10^5≤c_i≤10^5\)

输入样例:

6
5 1 6
1 4 5
6 3 9
2 6 8
6 1 7

输出样例:

22

二、朴素版本\(dfs\)【不能\(AC\)

朴素\(dfs\): 对每个点求最远点最大距离, 所有结果的\(max\)就是结果.
通过 \(11/17\). 然后\(TLE\), 效果不是很理想。

#include <bits/stdc++.h>
using namespace std;

const int N = 10010, M = N << 1;
//  暴力搜索,从每个节点为根出发,遍历整根树,找出距离自己的最大距离,然后每个最大距离取min
//   11/17,其它TLE,无法AC
int n;
int ans; // 树的直径
// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void dfs(int u, int fa, int sum) {
    if (sum > ans) ans = sum;
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue; // 不走回头路
        dfs(v, u, sum + w[i]);
    }
}

int main() {
    // 初始化邻接表
    memset(h, -1, sizeof h);

    cin >> n;
    for (int i = 1; i < n; i++) { // n-1条边
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c); // 无向图
    }

    // 多次dfs,是TLE的罪魁祸首
    for (int i = 1; i <= n; i++) dfs(i, 0, 0);

    // 输出结果
    printf("%d", ans);
    return 0;
}

三、两次\(dfs\)解法 【不能\(AC\)

优点:思路简单

缺点:只适用于不带负边的树

此题目中的第\(15\)组数据,可以\(HACK\)掉这种作法(此组数据中带有负权边)

算法:从任意点\(a\)出发, 找到距离\(a\)最远的点\(t1\), 然后从\(t1\)出发, 找到距离\(t1\)最远的点\(t2\),\(t1\)\(t2\)的距离即为我们要找到结果.

黄海注:在\(AcWing\) \(1073\)中,没有出现边权是负值的情况,所以两遍\(dfs\)大法好用 : 传送门

这里我就不赘述证明了,想看证明的同学可以移步洛谷里面一个题的题解,里面有证明: 传送门,或者看一下\(yxc\)本题的视频教程。

通过了 \(15/17\)个数据,剩余两个测试点,居然是\(WA\),真是,唉~

#include <bits/stdc++.h>

using namespace std;

const int N = 10010, M = N << 1;

int ans; // 保存最长路径
int t;   // 保存找到的最远点
int n;

// 邻接表
int e[M], h[N], idx, w[M], ne[M];
void add(int a, int b, int c = 0) {
    e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}

void dfs(int u, int fa, int sum) {
    if (sum > ans) {
        ans = sum; // 记录最大距离
        t = u;     // 记录最远的点t1
    }
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue;
        dfs(v, u, sum + w[i]);
    }
}

int main() {
    memset(h, -1, sizeof h);
    cin >> n;
    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, 0, 0); // 先找到点距离点1最远的点t1

    dfs(t, 0, 0); // 找到距离点t1->t2最远的点t1

    printf("%d", ans);
    return 0;
}

四、最长+次长解法【终极解法】

树的最长路径 ,也称为树的 直径 ,直径 不唯一

我们知道:树上 任意两点 的路径是 唯一 确定的,因此我们可以暴力枚举 起点终点 找出最长路径

如果这样做的话,我们来思考一下时间复杂度:

枚举 起点终点\(O(n^2)\)
找出两点之间的路径长度 — \(O(logn)\)

但是光是枚举 起点终点时间复杂度 就直接拉满了,显然这种做法不可取。

既然这 \(O(n^2)\) 条路径不能 一一枚举,那么有什么方式可以把他们 分类枚举 呢?

考虑换一种 枚举方式:枚举路径的 起点和终点 \(→\) 枚举路径的 中间节点

注:枚举中间节点非常妙,因为树节点只有\(n\)个,全遍历一遍也没啥问题,怕就怕双重循环的两两一组。如此,就成功的将双重循环\(O(N^2)\)时间复杂度降为\(O(N)\)的时间复杂度。方法就是在遍历过程中,努力构建关于当前节点\(u\)的多重信息,然后用这些信息去组装出直径最大值,果然有\(dp\)的味道在里面~

我们先讨论一下,对于给定拓扑结构的树里的任意节点,经过它的路径有哪些:

观察 红色节点【本质上就是对于树中的任意节点均同此理】,经过它的路径有:

  • 以其 子树中的某个节点 作为 起点,以它作为 终点粉色路径
  • 以其 子树中的某个节点 作为 起点,以 子树中的某个节点 作为 终点蓝色路径
  • 以其 子树中的某个节点 作为 起点,以 非其子树的节点 作为 终点橙色路径

对于第 \(1\) 种情况,可以 直接递归处理其子树,找出到当前子树根节点最长的路径长度即可
对于第 \(2\) 种情况,在处理第 \(1\) 种情况时,顺便找出 \(1\) 类路径的 次长路径,再把 最长次长 拼在一起,就是第 \(2\) 种情况
对于第 \(3\) 种情况,可以把它归类为其 祖先节点 的第 \(1,2\) 种情况,让其 祖先节点 去处理即可

实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 10010, M = N << 1;
int n; // n个结点

// 链式前向星
int h[N], e[M], w[M], ne[M], idx;
void add(int a, int b, int c) {
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

int ans;            // 答案,直径
int mx1[N], mx2[N]; // mx1[i],mx2[i]:经过i点的最长,次长长度是多少

void dfs(int u, int fa) {
    for (int i = h[u]; ~i; i = ne[i]) {
        int v = e[i];
        if (v == fa) continue; // v点访问过了

        // 走v子树,完成后,v子树中每个节点的mx1[v],mx2[v]都已经准备好,u节点可以直接利用
        dfs(v, u);

        // w[i]:u->v的路径长度,mx1[u]:最长路径,mx2[u]:次长路径
        int x = mx1[v] + w[i];
        if (mx1[u] <= x)                 // v可以用来更新u的最大值
            mx2[u] = mx1[u], mx1[u] = x; // 最长路转移
        else if (mx2[u] < x)
            mx2[u] = x; // 次长路转移
    }
    // 更新结果
    ans = max(ans, mx1[u] + mx2[u]);
}

int main() {
    cin >> n;
    memset(h, -1, sizeof h);      // 初始化邻接表
    for (int i = 1; i < n; i++) { // n-1条边
        int a, b, c;
        cin >> a >> b >> c;
        add(a, b, c), add(b, a, c); // 换根dp一般用于无向图
    }
    dfs(1, 0); // 任选一个点作为根节点,此处选择的是肯定存在的1号结点
    cout << ans << endl;
    return 0;
}
posted @ 2022-01-10 15:43  糖豆爸爸  阅读(520)  评论(0编辑  收藏  举报
Live2D