如何找到树上一组节点之间的最大距离?

Posted

技术标签:

【中文标题】如何找到树上一组节点之间的最大距离?【英文标题】:How to find the max distance between a set of nodes on a tree? 【发布时间】:2012-11-21 20:07:05 【问题描述】:

我在一棵(非二叉树)树上有一组 n 个节点。我想找到任意两个节点之间的最大距离。 (我将两个节点之间的距离定义为这些节点与其最低共同祖先之间的距离之和)。

我可以在 O(n^2) 中轻松解决这个问题,只需计算每个节点与其他节点之间的距离并获得最大值,但是我希望有更好的东西,因为这对我来说太慢了*应用场景。

(补充信息:在我的应用场景中,这些节点实际上是文件,树是目录结构。因此,树很浅(深度 之间。实际上,我试图弄清楚我的文件在每组中的分布范围。)*

编辑:也许我可以问一个等效的问题来提示更多答案:考虑原始树的一个子集,它只包含我的集合中的节点以及连接它们所需的节点。那么问题就变成了:如何在无向无环图中找到最长的简单路径?

*编辑 2: 正如 didierc 指出的那样,我实际上应该考虑文件夹集而不是文件。这使我的集合更小,并且详尽的方法可能足够快。尽管如此,看到一个更快的解决方案还是有好处的,我很想知道是否有一个。

【问题讨论】:

如果您认为您只是比较文件夹而不是文件(因为它们中的许多共享同一个文件夹),您可以摆脱详尽的比较。也就是说,n其实就是文件夹的个数。 首先建立一个你的树的内存模型,以避免被IO限制。 @didierc 我确实有一个内存模型。关于节点是文件夹而不是文件的要点;在这种情况下,详尽的方法实际上可能足够快。不过,我仍然很好奇是否有更快的解决方案。 你想要距离还是“违规”节点? @didierc 两者都有用。我认为找到距离会更简单,但是拥有“违规”节点可能同样有用。基本上,我只是尝试建立关于这些文件组的分布程度的指标。信息越多越好。 【参考方案1】:

您的问题也称为查找树的直径:在节点对之间的所有最短路径中,您正在寻找最长的。

用 d(S) 表示树 S 的直径,用 h(S) 表示树的高度。

具有子树 S1...Sd 的树 S 中最远的两个节点可以位于其中一个子树下,也可以跨越两个子树。在第一种情况下,当两个最远的节点在子树 Si 下时,d(S) 就是 d(Si)。在第二种情况下,当距离最远的两个节点跨越两个子树时,比如说 Si 和 Sj,它们的距离是 h(Si) + h(Sj) + 2,因为这两个节点必须是每个子树中最深的两个节点,再加上两个更多的边加入两个子树。事实上,在这第二种情况下,Si 和 Sj 一定是 S 的最高和第二高的子树。

一个 O(n) 算法将如下进行

算法 d(S)

1. recursively compute d(S1)...d(Sd) and h(S1)...h(Sd) of the subtrees of S.
2. denote by Si be the deepest subtree and Sj the second deepest subtree
3. return max(d(S1), ..., d(Sd), h(Si)+h(Sj)+2)

分析

第 2 行和第 3 行都需要 O(d) 时间来计算。但是这些行只检查每个节点一次,因此在递归中,这总共需要 O(n)。

【讨论】:

出于某种原因,它不允许我评论 @BlueRaja 的解决方案,所以我在这里发表评论。两个最远的节点可能位于同一子树下,在这种情况下,您的算法会错误地尝试跨越两个子树。例如,当根节点有两个子树时,您的算法会得到错误的直径,其中一个是非常深的平衡树,另一个是单个节点。 我的算法没有缺陷;事实上,我们的算法是完全一样的。 感谢您提供有关树木直径的信息;我不知道这个。我选择您的答案是因为它完整、有见地、易于理解且易于实施。 还有另一种很好的线性算法来查找直径:选择任意节点并在树上运行 DFS。取离起始节点最远的节点。从最远的节点运行 DFS。第二个 DFS 中的最大距离是直径。【参考方案2】:

我有一个简单的 O(n) 贪心算法来解决这个有趣的问题。

算法

    选择任意顶点X作为树的根,然后找到 与根的距离最大的顶点 Y X. 这一步的复杂度是O(n)。 将顶点Y作为树的新根,然后找到顶点Z 谁与根 Y 的距离最大。 Y 和 Z 之间的距离是树中距离的最大值。这一步的复杂度也是 O(n)。 这个贪心算法的总复杂度是 O(n)。

证明

显然 Y 和 Z 形成了树的一个直径,我们 将 Y 和 Z 称为树的一对角。 定理:对于树中的每个顶点 P,Y 或 Z 将是 与它的距离最大的顶点。 算法的第一步是基于定理,所以我们可以 轻松获得树的一个角 (Y)。 第二个角 Z 也是基于 定理 找到的。

扩展

根据证明中的定理,我们可以解决另一个更具挑战性的问题:对于树中的每个顶点,计算谁是它的最远顶点。

我们可以在 O(n) 复杂度中找到树的两个角,然后我们 可以再次使用定理。 从角 Y 和 Z 我们分别做 dfs,并且对于每个顶点 p[i] 我们可以得到到 Y 和 Z 的距离(我们称它们为 disY[i] 和 disZ[i]),所以 p[i] 的最远距离是 max(disY[i], disZ[i])。 由于我们只做了两次 dfs,所以我们可以得到 O(n) 中的信息 复杂性。 这个扩展问题也可以通过复杂树动态来解决 复杂度也是 O(n) 的编程。

【讨论】:

哇,我真的很欣赏这个解决方案的简单优雅。 很好的解决方案。这是它有效的证明。该算法找到一对节点 x0,y0 使得 max_x d(x,x0) = max_y d(x0,y) (即 x0 和 y0 是彼此最远的节点)。对于任何这样的对,d(x0,y0) 是直径。证明:设 x*,y* 为两个节点 s.t. d(x*,y*) 是直径。存在节点 r 和 s,因此路径看起来像 x0--r--s--y0 和 x*--r--s--y*。假设 d(x0,y0) d(x0,y0) 或 d(y*,x0) > d(y0,x0),矛盾x0 和 y0 是彼此最远的点这一事实。因此 d(x0,y0)=d(x*,y*)。【参考方案3】:

假设两个节点之间的最大长度路径通过我们的根节点。那么这两个节点中的一个必须属于一个孩子的子树,而另一个必须属于另一个孩子的子树。那么很容易看出,这两个节点是这两个孩子的最低/最深的后代,这意味着这两个节点之间的距离是height(child1) + height(child2) + 2。因此,通过我们的根的两个节点之间的最大长度路径是max-height-of-a-child + second-to-max-height-of-a-child + 2

这为我们提供了一个简单的 O(n) 算法来找到整个最大长度路径:只需对每个非叶节点执行上述操作。由于每条路径都必须植根于某个非叶子节点,这保证了我们会在某个时候考虑正确的路径。

找到一个子树的高度是 O(n),但是,因为你可以递归地建立高度,所以找到 每个 子树的高度也是 O(n)。事实上,您甚至不需要将高度作为一个单独的步骤来计算;您可以同时找到最大长度路径和子树高度,这意味着该算法只需要 O(n) 时间和 O(树高度) 空间。

【讨论】:

这个算法有一个细微的缺陷。两个最远的节点可能位于同一子树下,在这种情况下,您的算法仍会错误地尝试跨越两个子树。例如,当根节点有两个子树时,您的算法会得到错误的直径,其中一个是非常深的平衡树,另一个是单个节点。 @moos:不正确;如果两个节点都在同一个子树下,那么根节点将不是两个节点之间最长路径的一部分(也就是说,它不是这些节点的最大共同祖先)。在这种情况下,您将在递归 时找到最大的共同祖先(请记住,您需要跟踪在 所有 个非叶节点上运行此算法后找到的最大距离)。我已经更新了这个答案,希望能更清楚地说明这一点。 还有一个问题:+1 应该是 +2(跨越根需要两条边,而不仅仅是一条) @moos:呵呵,是的,我自己也注意到了,已经修复了:)【参考方案4】:

这是一种递归算法。这是伪代码(未经测试的ocaml代码):

 type result = n1 : node; n2 : node; d1 : int (* depth of node n1 *); d2 : int; distance: int
(* a struct containing:
    - the couple of nodes (n1,n2),
    - the depth of the nodes, with depth(n1) >= depth(n2)
    - the distance between n1 & n2 *)


let find_max (n : node) : result =
 let max (s1 : result) (s2 : result) = if s1.distance < s2.distance then s2 else s1 in
 let cl : node list = Node.children n in
 if cl = []
 then  n1 = n; n2 = n; d1 = 0; d2 = 0; distance = 0 
 else 
   let ml = List.map find_max cl in
   let nl = List.map (fun e -> e.n1, e.d1+1) ml in
   let (k1,d1)::(k2,d2)::nl = nl in
   let k1,d1,k2,d2 = if d1 > d2 then k1,d1,k2,d2 else k2,d2,k1,d1 in
   let s = n1 = k1;n2 = k2; d1 = d1; d2 = d2; distance = d1+d2 in
   let m1 =  List.fold_left (fun r (e,d) -> 
                      if r.d1< d
                      then  r with n1 = e; d1 = d; distance = d+d2 
                      else if r.d2 < d 
                               then  r with n2 = e; d2 = d; distance = d+d1 
                               else r) s nl in
   max m1 (List.fold_left max (List.hd ml) (List.tl ml))

m1 的值是通过保持 nl 列表中两个最深的节点(距离是它们的深度之和)来构建的。

List.map 是将给定函数应用于列表的所有元素并返回结果列表的函数。

List.fold_left 是一个函数,将给定函数递归地应用于累加器和列表的元素,每次都使用前一个应用程序的结果作为新的累加器值。结果是最后一个累加器值。

List.hd 返回列表的第一个元素。

List.tl 返回一个没有第一个元素的列表。

【讨论】:

以上是关于如何找到树上一组节点之间的最大距离?的主要内容,如果未能解决你的问题,请参考以下文章

二叉树上节点间的最大距离

最强解析面试题:二叉树两节点最大距离

如何选择最小化到图中其他节点的最大最短距离的节点?

P1351 联合权值

一组经度/纬度点之间的最大距离

[xJOI3335] 树的直径(树上最远点)