题目描述

[!green]

牛客原题: ✅ 补充题 144. 带权树的直径

给定由 n 个节点和 n-1 条带权无向边组成的树,节点编号为 0…n-1。每条边用 [u,v,w] 表示两个端点和边权。

返回两个节点之间路径权重和的最大值。路径不能重复经过节点;只有一个节点时返回 0。

代码使用 long / int64 累加。输入边若从 1 开始编号,调用前需统一减 1。

示例 1:

输入: n = 4, edges = [[0,1,5],[1,2,2],[1,3,4]]
输出: 9
解释: 路径 0→1→3 的权重和为 5+4=9,是所有非空路径中的最大值。

提示:

  • 2 <= n <= 100000
  • 保证最终结果满足 - 2^{31} <= val <= 2^{31}-1

题意分析

树中任意两个节点之间只有一条简单路径。将树定根后,一条路径要么完全位于某个子树中,要么经过当前节点,连接它下面的至多两条分支,因此可以自底向上合并。

节点数可能达到十万,用迭代遍历得到父子顺序,再逆序计算,避免链状树带来的深递归。

解法:迭代后序合并最佳树分支

核心思路

[!blue]

down[u] 保存从 u 向某个后代延伸的最大非负贡献,取 0 表示不再向下走。对孩子 v,经过边 (u,v) 的分支贡献为 branch = weight + down[v]。

处理每个孩子时,先用旧的 down[u] + branch 更新全局答案,再用 branch 更新 down[u]。旧值仅来自此前处理的其他孩子或空分支,因此组合不会在同一孩子子树中重复走,形成的是合法简单路径。

任意最优路径都会在其最高节点处被这种组合考虑,子树内部最优路径则已在逆序处理中记录。全局答案从最小整数开始,使全负边时仍返回一条实际边对应的负值;仅有一个节点按约定返回 0。

解题步骤

  1. 建立无向邻接表,迭代遍历得到父节点和处理顺序。
  2. 逆序处理节点,计算各孩子经当前节点延伸的分支权重。
  3. 合并两条最佳分支并更新全局答案,单节点返回0。

代码实现

class Solution {
    public long diameter(int n, int[][] edges) {
        if (n <= 1) {
            return 0;
        }

        ArrayList<int[]>[] graph = new ArrayList[n];

        for (int i = 0; i < n; i++) {
            graph[i] = new ArrayList<>();
        }

        for (int[] e : edges) {
            graph[e[0]].add(new int[] {
                e[1],
                e[2]
            });
            graph[e[1]].add(new int[] {
                e[0],
                e[2]
            });
        }

        int[] parent = new int[n];
        int[] order = new int[n];

        Arrays.fill(parent, -2);
        parent[0] = -1;
        int size = 1;

        for (int i = 0; i < size; i++) {
            int u = order[i];

            for (int[] e : graph[u]) {
                if (parent[e[0]] == -2) {
                    parent[e[0]] = u;
                    order[size++] = e[0];
                }
            }
        }

        long[] down = new long[n];
        long answer = Long.MIN_VALUE;

        for (int i = n - 1; i >= 0; i--) {
            int u = order[i];

            for (int[] e : graph[u]) {
                if (parent[e[0]] == u) {
                    long branch = e[1] + down[e[0]];

                    answer = Math.max(answer, down[u] + branch);
                    down[u] = Math.max(down[u], branch);
                }
            }
        }

        return answer;
    }
}
func diameter(n int, edges [][]int) int64 {
    if n <= 1 {
        return 0
    }
    type edge struct{ to, weight int }
    graph := make([][]edge, n)
    for _, e := range edges {
        graph[e[0]] = append(graph[e[0]], edge{e[1], e[2]})
        graph[e[1]] = append(graph[e[1]], edge{e[0], e[2]})
    }
    parent := make([]int, n)
    for i := range parent {
        parent[i] = -2
    }
    parent[0] = -1
    order := []int{
        0,
    }
    for i := 0; i < len(order); i++ {
        u := order[i]
        for _, e := range graph[u] {
            if parent[e.to] == -2 {
                parent[e.to] = u
                order = append(order, e.to)
            }
        }
    }
    down := make([]int64, n)
    answer := int64(-1 << 63)
    for i := n - 1; i >= 0; i-- {
        u := order[i]
        for _, e := range graph[u] {
            if parent[e.to] == u {
                branch := int64(e.weight) + down[e.to]
                answer = max(answer, down[u]+branch)
                down[u] = max(down[u], branch)
            }
        }
    }
    return answer
}

复杂度分析

  • 时间复杂度:$O(n)$。
  • 空间复杂度:额外空间 $O(n)$。

关键点总结

[!green]

两条分支只在 u 相交,构成一条合法简单路径;非正贡献不向上延伸;全局答案仍独立记录每条候选边,所以额外允许负边时也不会把“两个不同节点的路径”误算成空路径。

易错点总结

[!yellow]

树用无向边表示,必须跳过父节点;不能把层数当边权和;本文接口的0起始编号是明确适配,不冒充原题函数签名。

相似题目

题目 难度 关联与区别
1245. 树的直径 中等 原题按边数计距离;本题累加边权,支持负边时使用树形 DP 而不直接套最远点搜索。
124. 二叉树中的最大路径和 困难 复用单边贡献向上返回、双分支更新全局答案的思路;本题权重位于边而非节点。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/86625506
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!