Алгоритм Тарьяна поиска LCA за O(1) в оффлайн — различия между версиями
Алесандр (обсуждение | вклад) |
Dmozze (обсуждение | вклад) м (Исправлена описка) |
||
(не показано 28 промежуточных версий 4 участников) | |||
Строка 1: | Строка 1: | ||
− | Дано дерево и набор запросов: пары вершин <tex>\langle v, u \rangle </tex> | + | Дано [[Дерево, эквивалентные определения | дерево]] <tex> G </tex> и набор запросов: пары вершин <tex>\langle v, u \rangle </tex>. Для каждой пары нужно найти наименьшего общего предка. Считаем, что все запросы известны заранее, поэтому будем решать задачу оффлайн. |
− | Алгоритм позволяет найти ответы для дерева из <tex>n</tex> вершин и <tex>m</tex> запросов за время <tex>O (n + m)</tex>, то есть при достаточно большом <tex>m</tex> | + | Алгоритм позволяет найти ответы для дерева из <tex>n</tex> вершин и <tex>m</tex> запросов за время <tex>O (n + m)</tex>, то есть при достаточно большом <tex>m</tex> за <tex>O (1)</tex> на запрос. |
== Алгоритм == | == Алгоритм == | ||
Подвесим наше дерево за любую вершину, и запустим [[Обход в глубину, цвета вершин|обход в глубину]] из неё. | Подвесим наше дерево за любую вершину, и запустим [[Обход в глубину, цвета вершин|обход в глубину]] из неё. | ||
− | Ответ на каждый запрос мы найдём в течение поиска в глубину. Ответ для вершин <tex>v</tex> и <tex>u</tex> находится, когда мы уже посетили вершину <tex>u</tex>, а | + | Ответ на каждый запрос мы найдём в течение поиска в глубину. Ответ для вершин <tex>v</tex> и <tex>u</tex> находится, когда мы уже посетили вершину <tex>u</tex>, а также посетили всех сыновей вершины <tex>v</tex> и собираемся выйти из неё. |
Зафиксируем момент: мы собираемся выйти из вершины <tex>v</tex> (обработали всех сыновей) и хотим узнать ответ для пары <tex>\langle v</tex>, <tex>u \rangle</tex>. | Зафиксируем момент: мы собираемся выйти из вершины <tex>v</tex> (обработали всех сыновей) и хотим узнать ответ для пары <tex>\langle v</tex>, <tex>u \rangle</tex>. | ||
− | Тогда заметим, что ответ {{---}} это либо вершина <tex>u</tex>, либо какой-то её предок. Значит, нам нужно найти предка вершины <tex>v</tex>, который является предком вершины <tex>u</tex> с наибольшей глубиной. Заметим, что при фиксированном <tex>v</tex> каждый из предков вершины <tex>v</tex> порождает некоторый класс вершин <tex>u</tex>, для которых он является ответом, в этом классе содержатся все вершины которые находятся "слева" от этого предка. | + | Тогда заметим, что ответ {{---}} это либо вершина <tex>u</tex>, либо какой-то её предок. Значит, нам нужно найти предка вершины <tex>v</tex>, который является предком вершины <tex>u</tex> с наибольшей глубиной. Заметим, что при фиксированном <tex>v</tex> каждый из предков вершины <tex>v</tex> порождает некоторый класс вершин <tex>u</tex>, для которых он является ответом, в этом классе содержатся все вершины, которые находятся "слева" от этого предка. |
− | На рисунке | + | На рисунке непосещённые вершины раскрашены в белый цвет, а посещённые разбиты на классы, каждому из которых соответствует свой цвет. |
− | + | [[file:mytree.png|500px|разные цвета {{---}} разные классы, а белые вершины ещё не просмотренные в dfs]] | |
− | + | Классы этих вершин не пересекаются, а значит, мы можем их эффективно обрабатывать с помощью [[СНМ (реализация с помощью леса корневых деревьев)|системы непересекающихся множеств]], которую будем хранить в массиве <tex>dsu</tex>. | |
− | |||
− | |||
− | + | Будем поддерживать также массив <tex>lcaClass[1 \dots n]</tex>, где <tex> lcaClass[w] </tex> {{---}} наименьший общий предок всех вершин, которые лежат в том же классе, что и <tex> w </tex>. Обновление массива <tex> lcaClass </tex> для каждого элемента будет неэффективно. Поэтому зафиксируем в каждом классе какого-то представителя. Функция <tex> \mathrm{find}(w) </tex> вернёт представителя класса, в котором находится вершина <tex> w </tex>. Тогда наименьшим общим предком всех вершин из класса <tex> w </tex> будет вершина <tex> lcaClass[\mathrm{find}(w)] </tex>. | |
− | |||
− | + | Обновление массива <tex> lcaClass </tex> будем производить следующим образом: | |
− | + | * когда мы приходим в новую вершину <tex>v</tex>, мы должны добавить её в новый класс {{---}} <tex>lcaClass[v] = v</tex> | |
+ | * когда просмотрим всё поддерево какого-то ребёнка <tex> u </tex> у вершины <tex> v </tex>, мы должны объединить поддерево ребёнка с классом вершины <tex> v </tex> (<tex>\mathrm{union}(v, u, v)</tex> {{---}} объединить классы вершин <tex> v </tex> и <tex> u </tex>, а наименьшим общим предком представителя нового класса сделать вершину <tex> v </tex>). Система непересекающихся множеств сама определит представителя в зависимости от используемой нами эвристики. Нам надо лишь правильно установить значение массива <tex> lcaClass </tex> у нового представителя. | ||
− | + | После того как мы обработали всех детей вершины <tex>v</tex>, мы можем ответить на все запросы вида <tex>\langle v, u \rangle </tex>, где <tex>u</tex> {{---}} уже посещённая вершина. | |
+ | Нетрудно заметить, что <tex>lca(v, u) = lcaClass[\mathrm{find}(u)]</tex>. Для каждого запроса это условие (что одна вершина уже посещена, а другую мы обрабатываем) выполнится только один раз. | ||
=== Реализация === | === Реализация === | ||
'''bool''' visited[n] | '''bool''' visited[n] | ||
− | |||
− | + | '''function''' union(x : '''int''', y : '''int''', newAncestor : '''int'''): | |
− | + | leader = dsuUnion(x, y) <font color=green> // объединяем классы вершин <tex> x </tex> и <tex> y </tex> и получаем нового представителя класса </font> | |
− | + | lcaClass[leader] = newAncestor <font color=green> // устанавливаем нового предка представителю множества </font> | |
− | |||
− | |||
− | |||
− | |||
− | '''function''' union( | ||
− | |||
− | |||
− | |||
− | |||
− | <font color=green>// можно запустить от любой вершины дерева | + | <font color=green>// можно запустить от любой вершины дерева в самый первый раз</font> |
'''function''' dfs(v : '''int'''): | '''function''' dfs(v : '''int'''): | ||
− | visited[v] = ''true'' | + | visited[v] = ''true'' |
+ | lcaClass[v] = v | ||
'''foreach''' u : (v, u) '''in''' G | '''foreach''' u : (v, u) '''in''' G | ||
'''if''' '''not''' visited[u] | '''if''' '''not''' visited[u] | ||
dfs(u) | dfs(u) | ||
union(v, u, v) | union(v, u, v) | ||
− | '''for''' | + | '''for''' (u : <tex>\langle v, u \rangle </tex> {{---}} есть такой запрос) |
− | '''if''' visited[ | + | '''if''' visited[u] |
− | запомнить, что ответ для запроса <tex>\langle v, u \rangle </tex> = | + | запомнить, что ответ для запроса <tex>\langle v, u \rangle </tex> = lcaClass[find[u]] |
− | == | + | == Корректность == |
− | + | ||
+ | Случай, когда <tex> u </tex> является наименьшим общим предком вершин <tex> u </tex> и <tex> v </tex>, обработается правильно, потому что по алгоритму в этот момент <tex> lcaClass[\mathrm{find}(u)] = u </tex>. | ||
+ | |||
+ | Пусть теперь наименьшим общим предком вершин <tex> u </tex> и <tex> v </tex> будет вершина, отличная от этих двух. Во время обработки запроса алгоритм точно вернёт общего предка этих двух вершин, так как он будет предком одной из вершин по массиву <tex> lcaClass </tex>, а предком другой из-за обхода в глубину. | ||
+ | |||
+ | Покажем, что найдём наименьшего предка. Пусть это не так. Тогда существует какая-то вершина <tex> w </tex>, которая тоже является предком вершин <tex> u </tex> и <tex> v </tex>, и из которой мы вышли раньше во время обхода в глубину. Но тогда ситуация, что одна из вершин посещена, а у другой рассмотрены все дети, должна была выполниться раньше, и в качестве ответа должна была вернуться вершина <tex> w </tex>. | ||
− | + | '''Замечание:''' для корректности алгоритма достаточно было бы одного массива <tex> dsu </tex>, а представителем класса всегда выбирать наименьшего общего предка вершин класса. Это несложно сделать, так как мы всегда объединяем ребёнка со своим родителем. Но в таком случае алгоритм получился бы менее эффективным, потому что одна только эвристика сжатия путей работает недостаточно быстро. | |
− | |||
− | |||
− | |||
− | + | == Оценка сложности == | |
+ | Она состоит из нескольких частей. | ||
− | Каждый запрос <tex>\langle v, u \rangle </tex> будет рассмотрен дважды {{---}} при | + | *Обход в глубину выполняется за <tex>O(n)</tex>. |
+ | *Операции по объединению множеств, которые в сумме для всех разумных <tex>n</tex> работают <tex>O (n)</tex> времени. Каждый запрос <tex>\langle v, u \rangle </tex> будет рассмотрен дважды {{---}} при посещении вершины <tex>u</tex> и <tex>v</tex>, но обработан лишь один раз, поэтому можно считать, что все запросы обработаются суммарно за <tex>O (m)</tex>. | ||
+ | *Для каждого запроса проверка условия и определение результата, опять же, для всех разумных <tex>n</tex> выполняется за <tex>O (1)</tex>. | ||
− | + | Следовательно, итоговая асимптотика {{---}} <tex>O (n + m)</tex>, что при достаточно больших <tex>m</tex> составляет <tex>O (1)</tex> на один запрос. | |
− | + | ==См. также== | |
+ | * [[Сведение задачи LCA к задаче RMQ]] | ||
+ | * [[Heavy-light декомпозиция]] | ||
== Источники информации == | == Источники информации == |
Версия 22:52, 5 сентября 2019
Дано дерево и набор запросов: пары вершин . Для каждой пары нужно найти наименьшего общего предка. Считаем, что все запросы известны заранее, поэтому будем решать задачу оффлайн. Алгоритм позволяет найти ответы для дерева из вершин и запросов за время , то есть при достаточно большом за на запрос.
Содержание
Алгоритм
Подвесим наше дерево за любую вершину, и запустим обход в глубину из неё. Ответ на каждый запрос мы найдём в течение поиска в глубину. Ответ для вершин и находится, когда мы уже посетили вершину , а также посетили всех сыновей вершины и собираемся выйти из неё.
Зафиксируем момент: мы собираемся выйти из вершины
(обработали всех сыновей) и хотим узнать ответ для пары , . Тогда заметим, что ответ — это либо вершина , либо какой-то её предок. Значит, нам нужно найти предка вершины , который является предком вершины с наибольшей глубиной. Заметим, что при фиксированном каждый из предков вершины порождает некоторый класс вершин , для которых он является ответом, в этом классе содержатся все вершины, которые находятся "слева" от этого предка.На рисунке непосещённые вершины раскрашены в белый цвет, а посещённые разбиты на классы, каждому из которых соответствует свой цвет.
Классы этих вершин не пересекаются, а значит, мы можем их эффективно обрабатывать с помощью системы непересекающихся множеств, которую будем хранить в массиве .
Будем поддерживать также массив
, где — наименьший общий предок всех вершин, которые лежат в том же классе, что и . Обновление массива для каждого элемента будет неэффективно. Поэтому зафиксируем в каждом классе какого-то представителя. Функция вернёт представителя класса, в котором находится вершина . Тогда наименьшим общим предком всех вершин из класса будет вершина .Обновление массива
будем производить следующим образом:- когда мы приходим в новую вершину , мы должны добавить её в новый класс —
- когда просмотрим всё поддерево какого-то ребёнка у вершины , мы должны объединить поддерево ребёнка с классом вершины ( — объединить классы вершин и , а наименьшим общим предком представителя нового класса сделать вершину ). Система непересекающихся множеств сама определит представителя в зависимости от используемой нами эвристики. Нам надо лишь правильно установить значение массива у нового представителя.
После того как мы обработали всех детей вершины
, мы можем ответить на все запросы вида , где — уже посещённая вершина. Нетрудно заметить, что . Для каждого запроса это условие (что одна вершина уже посещена, а другую мы обрабатываем) выполнится только один раз.Реализация
bool visited[n] function union(x : int, y : int, newAncestor : int): leader = dsuUnion(x, y) // объединяем классы вершини и получаем нового представителя класса lcaClass[leader] = newAncestor // устанавливаем нового предка представителю множества // можно запустить от любой вершины дерева в самый первый раз function dfs(v : int): visited[v] = true lcaClass[v] = v foreach u : (v, u) in G if not visited[u] dfs(u) union(v, u, v) for (u : — есть такой запрос) if visited[u] запомнить, что ответ для запроса = lcaClass[find[u]]
Корректность
Случай, когда
является наименьшим общим предком вершин и , обработается правильно, потому что по алгоритму в этот момент .Пусть теперь наименьшим общим предком вершин
и будет вершина, отличная от этих двух. Во время обработки запроса алгоритм точно вернёт общего предка этих двух вершин, так как он будет предком одной из вершин по массиву , а предком другой из-за обхода в глубину.Покажем, что найдём наименьшего предка. Пусть это не так. Тогда существует какая-то вершина
, которая тоже является предком вершин и , и из которой мы вышли раньше во время обхода в глубину. Но тогда ситуация, что одна из вершин посещена, а у другой рассмотрены все дети, должна была выполниться раньше, и в качестве ответа должна была вернуться вершина .Замечание: для корректности алгоритма достаточно было бы одного массива
, а представителем класса всегда выбирать наименьшего общего предка вершин класса. Это несложно сделать, так как мы всегда объединяем ребёнка со своим родителем. Но в таком случае алгоритм получился бы менее эффективным, потому что одна только эвристика сжатия путей работает недостаточно быстро.Оценка сложности
Она состоит из нескольких частей.
- Обход в глубину выполняется за .
- Операции по объединению множеств, которые в сумме для всех разумных работают времени. Каждый запрос будет рассмотрен дважды — при посещении вершины и , но обработан лишь один раз, поэтому можно считать, что все запросы обработаются суммарно за .
- Для каждого запроса проверка условия и определение результата, опять же, для всех разумных выполняется за .
Следовательно, итоговая асимптотика —
, что при достаточно больших составляет на один запрос.