我在(非二进制)树上有一组n个节点。我想找到任意两个节点之间的最大距离。 (我将两个节点之间的距离定义为这些节点与其最低共同祖先之间距离的总和)。
我可以通过计算每个节点与其他节点之间的距离并获得最大值来轻松解决O(n ^ 2)中的这个问题,但是我希望能有更好的东西,因为这对我来说太慢了*应用场景。
(额外信息:在我的应用场景中,这些节点实际上是文件而树是目录结构。因此,树很浅(深度<~10),但它可能有300,000多个节点这些文件集的大小可以在~3-200 之间。实际上,我正在试图弄清楚每组文件的分布范围。)*
编辑:也许我可以提出一个等效问题来提示更多答案:考虑原始树的一个子集,它只包含我的集合中的节点和连接它们所需的节点。然后问题变成:如何在无向的非周期图中找到最长的简单路径?
* 编辑2:正如didierc指出的那样,我实际上应该考虑的是文件夹集而不是文件。这使得我的设置更小,并且详尽的方法可能足够快。不过,看到更快的解决方案是有益的,我很想知道是否有一个。
答案 0 :(得分:9)
您的问题也称为寻找树的直径:在节点对之间的所有最短路径中,您寻找的时间最长。
用d(S)表示树S的直径,用h(S)表示树的高度。
树S中具有子树S1 ... Sd的两个最远节点可以在其子树之一下,或者它们可以跨越两个子树。在第一种情况下,当两个最远的节点在子树Si下时,d(S)只是d(Si)。在第二种情况下,当两个最远距离节点跨越两个子树,比如Si和Sj时,它们的距离是h(Si)+ h(Sj)+ 2,因为两个节点必须是每个子树中最深的两个节点,加上两个更多边加入两个子树。实际上,在第二种情况下,Si和Sj必须是S中最高和第二最高的子树。
O(n)算法将按如下方式进行
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)。
答案 1 :(得分:4)
假设两个节点之间的最大长度路径通过我们的根节点。然后,两个节点中的一个必须属于一个子节点,另一个节点必须属于不同子节点的子树。那么很容易看出,这两个节点是这两个子节点的最低/最深的后代,这意味着这两个节点之间的距离为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(树高)空间。
答案 2 :(得分:4)
我有一个简单的O(n)贪婪算法来解决这个有趣的问题。
根据证明中的定理,我们可以解决另一个更具挑战性的问题:对于树中的每个顶点,计算谁是它的远端顶点。
答案 3 :(得分:3)
我将草拟一个递归遍历树的算法,并为每个节点计算子集中的两个最远的孩子。
让S成为节点的子集。对于每个节点,我们引入两个变量,存储S中子项的最长和第二长路径的长度。longest
和secondlongest
初始化为0。
for each child node // we'll skip this if we are a leaf
do recursive call
update longest and secondlongest // use return value of child call
if longest >= 1 // there is a node of S below us
return longest + 1 // increment length
if node in S // we found the first node of S on this path
return 1
return 0 // there is now node of S below us
现在,每个节点都知道到S中孩子的最长和第二长距离(它们可以相等)。再次遍历树并获得longest
和secondlongest
的最大总和。
整个算法在 O(n)。您还可以在主算法中获得最大总和,以避免第二次遍历。
答案 4 :(得分:1)
这是一种递归算法。这是伪代码(未经测试的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
返回没有第一个元素的列表。