AcWing 1072. 树的最长路径

AcWing 1072 树的最长路径

一、题目描述

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

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

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

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

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

接下来 n1 行,每行包含三个整数 ai,bi,ci,表示点 aibi 之间存在一条权值为 ci 的边。

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

数据范围
1n10000,
1ai,bin,
105ci105

输入样例:

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,t1t2的距离即为我们要找到结果.

黄海注:在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(n2)
找出两点之间的路径长度 — O(logn)

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

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

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

注:枚举中间节点非常妙,因为树节点只有n个,全遍历一遍也没啥问题,怕就怕双重循环的两两一组。如此,就成功的将双重循环O(N2)时间复杂度降为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 @   糖豆爸爸  阅读(1022)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2020-01-10 大数据平台组技术路线执行理念
2018-01-10 CAS单点登陆的两个原理图
2016-01-10 清除图片周围的空白区域
2015-01-10 Bringing up interface eth0: Determining if ip address 10.109.67.81 is already in use for device eth0...
Live2D
点击右上角即可分享
微信分享提示