Как найти максимальное расстояние между узлами на дереве?

У меня есть набор из N узлов (не бинарного) дерева. Я хочу найти максимальное расстояние между любыми двумя узлами. (Я определяю расстояние между двумя узлами как сумму расстояний между этими узлами и их наименьшим общим предком).

Я могу легко решить эту проблему в O (n^2), просто вычисляя расстояние между каждым узлом друг к другу и получая максимум, однако я надеюсь на что-то лучшее, поскольку это слишком медленно* для моего приложения сценарий.

(дополнительная информация: в моем сценарии приложения эти узлы на самом деле являются файлами, а дерево-структурой каталогов. Поэтому дерево довольно мелкое (глубина в размер. Фактически, я пытаюсь выяснить, насколько широко распространены мои файлы в каждом наборе.)*

Edit: возможно, я могу задать эквивалентный вопрос, чтобы предложить больше ответов: рассмотрим подмножество исходного дерева, содержащее только узлы в моем наборе и узлы, необходимые для их подключения. Тогда возникает вопрос:--9-->как найти самый длинный простой путь в неориентированном ациклическом графе?

*Edit 2: как указал дидьерк, я на самом деле должен рассматривать наборы папок, а не файлов. Это делает мои наборы меньше, и исчерпывающий подход может быть достаточно быстрым. Тем не менее, было бы полезно увидеть более быстрое решение, и мне любопытно посмотреть, есть ли он.

5 ответов


ваша проблема также известна как поиск диаметра дерева: среди всех кратчайших путей между парами узлов вы ищете самый длинный.

обозначим диаметр дерева S Через d(S), а его высоту-через h (S).

два самых удаленных узла в дереве S с поддеревьями S1...Sd могут находиться либо под одним из его поддеревьев, либо они могут охватывать два поддерева. В первом случае, когда два самых удаленных узла находятся под поддеревом 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).


предположим, что путь максимальной длины между двумя узлами проходит через наш корневой узел. Затем один из двух узлов должен принадлежать поддереву одного ребенка, а другой-поддереву другого ребенка. Тогда легко видеть, что эти два узла являются самыми низкими / самыми глубокими потомками этих двух детей, что означает, что расстояние между этими двумя узлами 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(высота дерева) пространства.


Я собираюсь набросать алгоритм, который обходит дерево и рекурсивно вычисляет двое самых дальних детей что это в вашей подгруппе, для каждого узла.

пусть 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). Вы также можете получить максимальную сумму в основном алгоритме, чтобы избежать второго обхода.


У меня есть простой o(n) жадный алгоритм для этой интересной проблемы.

алгоритм

  1. выберите произвольную вершину X в качестве корня дерева, а затем найдите вершина Y, которая имеет максимальное расстояние с корнем X. сложность этого шага равна O (n).
  2. сделайте вершину Y как новый корень дерева, затем найдите вершину Z кто имеет максимальное расстояние с корнем Y. А расстояние между Y и Z является максимальным из расстояний в дерево. Сложность этого шага также O (n).
  3. общая сложность этого жадного алгоритма равна O (n).

доказательство

  • очевидно, что Y и Z образуют один диаметр дерева, и мы назовем Y и Z парой углов дерева.
  • Теорема: для каждой вершины P в дереве либо Y, либо Z будет вершины, который имеет максимальное расстояние с ним.
  • Шаг 1 алгоритма основан на Теорема, так что мы можем легко получить один угол (Y) дерева.
  • второй угол Z находится на основе Теорема как хорошо.

расширения

по словам Теорема в доказательстве мы можем решить еще одну более сложную проблему: для каждой вершины дерева вычислите, кто является fareast vertice к нему.

  • мы можем найти два угла дерева в o (n) сложности, а затем мы мочь используйте Теорема снова.
  • из угла Y и Z делаем dfs соответственно, и для каждой вершины p[i] мы можем получить расстояние до Y и Z(мы называем их disY[i] и disZ[i]), поэтому расстояние фариста для p[i] равно max(disY[i], disZ[i]). Из-за того, что мы просто делаем dfs дважды, поэтому мы можем получить информацию в O(n) сложность.
  • эта расширенная проблема может решена с помощью дерева complexible динамические Программирование, сложность которого также O (n).

это рекурсивный алгоритм. Вот псевдо-код (непроверенный код 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 возвращает список без первого элемента.