Diameter of a tree

本文介绍了一种线性时间复杂度的算法,用于寻找给定树中最长路径的两端点。通过对树特性的分析,利用两次广度优先搜索(BFS)找到树的直径,避免了使用Dijkstra's算法或Bellman-Ford算法。

简介

  在之前的文章里我讨论过计算图里最短路径的几种方法,一个是Dijkstra's algorithm,一个是Bellman-Ford Algorithm。它们都是针对一个比较通用形式的图来进行计算处理的。在实际的应用中,有些比较特殊的图,在计算它们的最短路径的应用中往往还有一些更加简单的方法。

 

问题描述 

  Given a graph that is a tree (connected and acyclic), find the longest path, i.e., a pair of vertices v and w that are as far apart as possible. Your algorithm should run in linear time.

 

问题分析

  这个问题的描述里既说到了图也提到了树。从概念上来说,树是图的一个特殊的形式,对于一个连通的无环图来说,它就是一棵树。而要计算一棵树或者图的直径,我们需要计算图里面最长的路径。在不考虑树这种特殊的结构的情况下,我们可以直接运用Dijkstra's algorithm或者Bellman-Ford algorithm。不过这两种算法的时间复杂度都不是单纯的线性时间。

  所以到这里我们就知道,不能直接用前面的算法来生搬硬套。看来能优化的地方就在于树这个前提了。以下图的树为例:

  我们要求一个节点到其他节点的距离或者最长最短距离,它们都有一个比较有意思的特性。从一个节点到另外一个节点只有一条且唯一的一条路径。这里基于一个不重复访问原有节点的情况下。 所以我们要计算一个节点到其他所有节点的距离,只需要采用一种方式遍历树就可以了。

  这个树的关键特性在于从给定的一个任意起点来说,它到其他节点都不可能构成环。所以它就不可能在通过其他的路径再访问到它访问过的节点。这样就不存在有我后面会访问到之前遍历过的节点。

   现在假设给定了一个点,我们计算出来了它到所有其他节点的距离。然后呢?这一步对于问题的解决有什么作用呢?假设在上图的示例中,我们选定了最初始的节点7:

  从它作为起始点开始,假定我们选择BFS作为遍历计算的方法。我们最终找到距离它最远的节点9:

  这一步找到了距离7最远的节点9。这一步有什么作用呢?在最简化的情况,假设这个最远的节点和源节点分别在根节点的两个子树中。那么这个目标节点相对于根节点来说一定是在这个子树中距离最长的点。如果要求树的直径的话,这边就相当于找到了一个点。

  对于另外一种情况呢?假设这个最远的节点和源节点都在根节点的一个子树中。那么它们构成的这个最长路径肯定是从源节点到一个它们的最近公共父节点,然后再往叶节点方向到另外一个节点。在这种情况下,这个最长距离节点和源节点到它们公共父节点的距离之和肯定是当前最大的。假定以这个最近公共父节点作为一个树的根,那么这个问题就可以概括成上一个情况。

  对于树来说,从一个非根节点到另外一个节点的最长距离很可能是通过根节点并到达另外一个子树的叶节点的长度。其实这一步相当于间接的求出来了从根节点到其他节点的最大距离。而下一步则需要根据当前这个节点9计算它到所有其他节点的距离。如果找到了所有距离值最大的那个,就找到了图的直径。

 

实现考虑

  基于前面的讨论,概括起来就是两个广度优先遍历的方式可以找到这个树的直径。在一般的BFS实现里,我们只是用一个boolean数组来表示访问过元素的标记。这里需要一个额外的数组int[] dist,用来记录从源节点到目标节点的距离。每次遍历的时候碰到新的节点就计算它到源节点的距离。然后在遍历完之后查找距离最远的节点,作为直径的一个节点。

  剩下的事情就是根据这个节点再次重复上面的过程找距离最长的点,这样就找到了直径节点和值了。

  其实这部分的代码比较简单,这里就不详细列出来了。

 

总结

  树是一种特殊的图,所以求树的直径可以用一种更加高效的办法,而不需要直接搬用传统的Dijkstra's 算法或者Bellman-Ford算法。这里为什么随意给定一个节点找到它距离最远的点就是直径的一个点的问题还有一些小的地方没有深入分析。需要进一步思考。

 

参考材料

algorithms

### 二叉树直径算法中返回 `ans-1` 的原因分析 在二叉树直径的计算中,`ans` 表示的是经过节点的最大数量。然而,题目要求的是路径的长度,而不是节点的数量。路径的长度定义为路径上边的数量,而边的数量总是比节点的数量少 1。因此,在最终返回结果时,需要将节点数减去 1,以得到路径的长度[^1]。 具体来说,算法通过递归计算每个节点的左右子树深度,并更新全局变量 `ans`,记录当前最大路径上的节点数。当递归完成后,`ans` 存储的是最长路径上的节点总数。由于路径长度等于节点数减去 1,所以返回值为 `ans - 1`[^5]。 以下是用 Python 实现的代码示例,展示了如何通过递归计算二叉树直径并返回路径长度: ```python # Definition for a binary tree node. class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = right class Solution: def diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int: ans = 0 def dfs(node): if not node: return -1 # 返回 -1 是为了简化路径长度计算 l_len = dfs(node.left) + 1 r_len = dfs(node.right) + 1 nonlocal ans ans = max(ans, l_len + r_len) # 更新最大路径上的节点数 return max(l_len, r_len) # 返回当前节点的深度 dfs(root) return ans # 最终返回的是节点数,但题目要求路径长度,因此不需要额外减 1 ``` 在上述代码中,`ans` 记录的是路径上的节点数,而在最终返回时直接返回 `ans` 即可,因为路径长度已经通过节点数减 1 的方式在递归过程中隐式处理了[^5]。 ### 注意事项 如果实现中直接使用节点数作为返回值,则需要明确说明路径长度节点数的关系,并在最终结果中减去 1,以符合题目对路径长度的定义[^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值