Level Ancestor problem — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
(Наивная реализация и двоичные подъемы)
(не показано 11 промежуточных версий этого же участника)
Строка 5: Строка 5:
 
найти предка вершины <tex>v</tex>, который находится на расстоянии <tex>k</tex> от корня дерева <tex>T</tex>.
 
найти предка вершины <tex>v</tex>, который находится на расстоянии <tex>k</tex> от корня дерева <tex>T</tex>.
 
}}
 
}}
== Наивная реализация и двоичные подъемы ==
+
== Использование Heavy-light декомпозиции ==
 
[[Файл:LevelAncestor.png|200px|thumb|right]]
 
[[Файл:LevelAncestor.png|200px|thumb|right]]
Используя обход в глубину посчитаем глубину каждой вершины дерева (это можно сделать за <tex>O(n)</tex>), после чего можем из вершины <tex>v</tex> подняться до необходимой глубины вершины <tex>k</tex>,
 
что так же в худшем случае работает за <tex>O(n)</tex>.
 
Получили алгоритм за < <tex>O(n), O(n)</tex> > времени и <tex>O(n)</tex> памяти, где время ответа на
 
запрос можно улучшить до <tex>O(\log n)</tex> c помощью [[Метод двоичного подъёма | предподсчета двоичных подъемов]]  ,
 
но тогда и время предподсчета в наивной реализации (посчитать подъемы для всех вершин) ухудшится до < <tex>O(n \log n),
 
O(\log n)</tex> > времени и <tex>O(n \log n)</tex> памяти. Альтернативой данным двум алгоритмам является полный предподсчет всех возможных запросов, что соответственно дает нам асимптотику < <tex>O(n^2), O(1)</tex> > времени и <tex>O(n^2)</tex> памяти.
 
 
В данном примере поступает запрос <tex>LA(v, 2)</tex>, на который алгоритм должен дать ответ <tex>h</tex>.
 
 
== Использование Heavy-light декомпозиции ==
 
 
Этот алгоритм базируется на различных способах [[Heavy-light декомпозиция | декомпозиции дерева]] (выберем heavy-light декомпозицию), из свойств этого разбиения следует,
 
Этот алгоритм базируется на различных способах [[Heavy-light декомпозиция | декомпозиции дерева]] (выберем heavy-light декомпозицию), из свойств этого разбиения следует,
 
что подняться на любую высоту из вершины <tex>v</tex> мы можем за время <tex>O(\log n)</tex>.
 
что подняться на любую высоту из вершины <tex>v</tex> мы можем за время <tex>O(\log n)</tex>.
 
Данное разбиение можно строить за <tex>O(n)</tex>, что дает нам алгоритм за < <tex>O(n), O(\log n)</tex> >.
 
Данное разбиение можно строить за <tex>O(n)</tex>, что дает нам алгоритм за < <tex>O(n), O(\log n)</tex> >.
 +
 +
В данном примере поступает запрос LA(v,2), на который алгоритм должен дать ответ h.
  
 
== Алгоритм лестниц ==
 
== Алгоритм лестниц ==
=== Longest path decomposition ===
+
===[https://www.mi.fu-berlin.de/en/inf/groups/abi/teaching/lectures/lectures_past/WS0910/V____Discrete_Mathematics_for_Bioinformatics__P1/material/scripts/treedecomposition1.pdf Longest path decomposition] ===
 
Разобьем все вершины на пути следующим образом. Обойдем дерево с помощью обхода в глубину, пусть мы стоим в вершине
 
Разобьем все вершины на пути следующим образом. Обойдем дерево с помощью обхода в глубину, пусть мы стоим в вершине
 
<tex>v</tex>, обойдем всех ее детей, добавив <tex>v</tex> в путь, идущий в самое глубокое поддерево,
 
<tex>v</tex>, обойдем всех ее детей, добавив <tex>v</tex> в путь, идущий в самое глубокое поддерево,
Строка 60: Строка 52:
  
 
В итоге полученный алгоритм действительно работает за < <tex>O(n), O(1)</tex> > времени и за <tex>O(n)</tex> памяти.
 
В итоге полученный алгоритм действительно работает за < <tex>O(n), O(1)</tex> > времени и за <tex>O(n)</tex> памяти.
 +
== Сравнение с наивными реализациями ==
 +
Используя <tex>dfs</tex> посчитаем глубину каждой вершины дерева (это можно сделать за <tex>O(n)</tex>), после чего можем из вершины <tex>v</tex> подняться до необходимой глубины вершины <tex>k</tex>,
 +
что так же в худшем случае работает за <tex>O(n)</tex>.
 +
Получили алгоритм за < <tex>O(n), O(n)</tex> > времени и <tex>O(n)</tex> памяти, где время ответа на
 +
запрос можно улучшить до <tex>O(\log n)</tex> c помощью [[Метод двоичного подъёма | предподсчета двоичных подъемов]]  ,
 +
но тогда и время предподсчета в наивной реализации (посчитать подъемы для всех вершин) ухудшится до < <tex>O(n \log n),
 +
O(\log n)</tex> > времени и <tex>O(n \log n)</tex> памяти. Также альтернативой данным двум алгоритмам является полный предподсчет всех возможных запросов, что соответственно дает нам асимптотику < <tex>O(n^2), O(1)</tex> > времени и <tex>O(n^2)</tex> памяти.
  
 +
Таким образом, самым оптимальным из описанных как по времени, так и по памяти является алгоритм Macro-Micro-Tree.
 +
 +
== См. также ==
 +
*[[Метод двоичного подъёма]]
 +
*[[Heavy-light декомпозиция]]
 
== Источники информации ==
 
== Источники информации ==
 
*[https://www.cadmo.ethz.ch/education/lectures/HS18/SAADS/reports/5.pdf Level Ancestor problem simplified Cai Qi]
 
*[https://www.cadmo.ethz.ch/education/lectures/HS18/SAADS/reports/5.pdf Level Ancestor problem simplified Cai Qi]
*[https://en.wikipedia.org/wiki/Level_ancestor_problem Wikipedia]
+
*[https://en.wikipedia.org/wiki/Level_ancestor_problem Wikipedia: LA]
 
+
*[https://www.mi.fu-berlin.de/en/inf/groups/abi/teaching/lectures/lectures_past/WS0910/V____Discrete_Mathematics_for_Bioinformatics__P1/material/scripts/treedecomposition1.pdf Longest path decomposition]
 
[[Категория: Алгоритмы и структуры данных]]
 
[[Категория: Алгоритмы и структуры данных]]

Версия 18:05, 15 мая 2019

Задача о уровне предка (англ. "Level Ancestor problem") является задачей о превращении данного корневого подвешенного дерева [math]T[/math] в структуру данных, которая сможет определить предка любого узла на заданном расстоянии от корня дерева.


Задача:
Дано корневое подвешенное дерево [math]T[/math] c [math]n[/math] вершинами. Поступают запросы вида [math]LA(v, k)[/math], для каждого из которых необходимо найти предка вершины [math]v[/math], который находится на расстоянии [math]k[/math] от корня дерева [math]T[/math].

Использование Heavy-light декомпозиции

LevelAncestor.png

Этот алгоритм базируется на различных способах декомпозиции дерева (выберем heavy-light декомпозицию), из свойств этого разбиения следует, что подняться на любую высоту из вершины [math]v[/math] мы можем за время [math]O(\log n)[/math]. Данное разбиение можно строить за [math]O(n)[/math], что дает нам алгоритм за < [math]O(n), O(\log n)[/math] >.

В данном примере поступает запрос LA(v,2), на который алгоритм должен дать ответ h.

Алгоритм лестниц

Longest path decomposition

Разобьем все вершины на пути следующим образом. Обойдем дерево с помощью обхода в глубину, пусть мы стоим в вершине [math]v[/math], обойдем всех ее детей, добавив [math]v[/math] в путь, идущий в самое глубокое поддерево, т.е. в котором находится вершина с самой большой глубиной. Для каждой вершины сохраним номер пути в который она входит.

Ladder decomposition

Увеличим каждый путь в два раза вверх, для каждого нового пути сохраним все входящие в него вершины, а для каждой вершины сохраним ее номер в пути, в который она входит. Построение обычной longest-path декомпозиции займет у нас [math]O(n)[/math] времени (обход в глубину), соответственно удлиннение каждого пути ухудшит асимптотику до [math]O(n \log n)[/math].

После этого посчитаем двоичные подъемы для каждой вершины за [math]O(\log n)[/math], что соответственно не ухудшит асимптотику.

Псевдокод

Пусть после этого нам пришел запрос [math]LA(v, k)[/math].

  function LA(int v,int k):
     int n = h(v); // получаем глубину вершины [math]v[/math]
     n = n - k;  // на столько необходимо подняться до ответа
     i = [math]\log n[/math];  
     v = p_i[v]  // делаем максимально большой прыжок вверх
     i = n - i;  // на столько осталось еще подняться
     return way[num_on_way[v] - i]; // так как теперь [math]v[/math] и ответ находятся на одном пути

Доказательство корректности

Рассмотрим путь, на котором лежит вершина [math]v[/math] до удвоения. Он длины хотя бы [math]2^i[/math], так как мы точно знаем, что существует вершина потомок [math]v[/math], расстояние до которого ровно [math]2^i[/math] (это вершина, из которой мы только что пришли). Значит, после удвоения этот путь стал длины хотя бы [math]2^{i + 1}[/math], причем хотя бы [math]2^i[/math] вершин в нем - предки [math]v[/math]. Это означает, что вершина, которую мы ищем, находится на этом пути (иначе бы мы могли до этого прыгнуть еще на [math]2^i[/math] вверх). Так как мы знаем позицию [math]v[/math] в этом пути, то нужную вершину мы можем найти за [math]O(1)[/math].

Таким образом, наш алгоритм работает за < [math]O(n\log n), O(1)[/math] > времени и за [math]O(n\log n)[/math] памяти. Методом четырех русских данный метод можно улучшить до < [math]O(n), O(1)[/math] > с помощью оптимизации предподсчета.

The Macro-Micro-Tree Algorithm

В данном разделе мы докажем, что предподсчет предыдущего алгоритма можно улучшить до [math]O(n)[/math]. Для начала рассмотрим алгоритм < [math]O(L\log n + n), O(1)[/math] >, где [math]L[/math] это количество листьев.

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

Рассмотрим как можно улучшить данный алгоритм:

  • Зададим некую функцию [math]S(n) = \dfrac{1}{4} \log n[/math]
  • Посчитаем размер поддерева для каждой вершины с помощью обхода в глубину, после чего удалим все вершины размер поддерева которых меньше чем [math]S(n)[/math].
  • Забудем на время про удаленные поддеревья, для оставшегося дерева наш алгоритм работает за < [math]O(\dfrac{n}{S(n)} \log n + n), O(1)[/math] >. Получаем алгоритм < [math]O(n), O(1) [/math] >. Для удаленных поддеревьев же выполним полный предподсчет: таких деревьев не более чем [math]2^{2S(n)}[/math], что дает асимптотику предподсчета [math]O(\sqrt{n} \log^2{n}) = o(n) = O(n)[/math].

В итоге полученный алгоритм действительно работает за < [math]O(n), O(1)[/math] > времени и за [math]O(n)[/math] памяти.

Сравнение с наивными реализациями

Используя [math]dfs[/math] посчитаем глубину каждой вершины дерева (это можно сделать за [math]O(n)[/math]), после чего можем из вершины [math]v[/math] подняться до необходимой глубины вершины [math]k[/math], что так же в худшем случае работает за [math]O(n)[/math]. Получили алгоритм за < [math]O(n), O(n)[/math] > времени и [math]O(n)[/math] памяти, где время ответа на запрос можно улучшить до [math]O(\log n)[/math] c помощью предподсчета двоичных подъемов , но тогда и время предподсчета в наивной реализации (посчитать подъемы для всех вершин) ухудшится до < [math]O(n \log n), O(\log n)[/math] > времени и [math]O(n \log n)[/math] памяти. Также альтернативой данным двум алгоритмам является полный предподсчет всех возможных запросов, что соответственно дает нам асимптотику < [math]O(n^2), O(1)[/math] > времени и [math]O(n^2)[/math] памяти.

Таким образом, самым оптимальным из описанных как по времени, так и по памяти является алгоритм Macro-Micro-Tree.

См. также

Источники информации