Алгоритм Тарьяна поиска LCA за O(1) в оффлайн
Дано дерево и набор запросов: пары вершин . Для каждой пары нужно найти наименьшего общего предка. Считаем, что все запросы известны заранее, поэтому будем решать задачу оффлайн. Алгоритм позволяет найти ответы для дерева из вершин и запросов за время , то есть при достаточно большом за на запрос.
Алгоритм
Подвесим наше дерево за любую вершину, и запустим обход в глубину из неё. Ответ на каждый запрос мы найдём в течение поиска в глубину. Ответ для вершин и находится, когда мы уже посетили вершину , а также посетили всех сыновей вершины и собираемся выйти из неё.
Зафиксируем момент: мы собираемся выйти из вершины (обработали всех сыновей) и хотим узнать ответ для пары , . Тогда заметим, что ответ — это либо вершина , либо какой-то её предок. Значит, нам нужно найти предка вершины , который является предком вершины с наибольшей глубиной. Заметим, что при фиксированном каждый из предков вершины порождает некоторый класс вершин , для которых он является ответом, в этом классе содержатся все вершины, которые находятся "слева" от этого предка.
На рисунке непосещённые вершины раскрашены в белый цвет, а посещённые разбиты на классы, каждому из которых соответствует свой цвет.
Классы этих вершин не пересекаются, а значит, мы можем их эффективно обрабатывать с помощью системы непересекающихся множеств, которую будем хранить в массиве .
Будем поддерживать также массив , где — наименьший общий предок всех вершин, которые лежат в том же классе, что и . Обновление массива для каждого элемента будет неэффективно. Поэтому зафиксируем в каждом классе какого-то представителя. Функция вернёт представителя класса, в котором находится вершина . Тогда наименьшим общим предком всех вершин из класса будет вершина .
Обновление массива будем производить следующим образом:
- когда мы приходим в новую вершину , мы должны добавить её в новый класс —
- когда просмотрим всё поддерево какого-то ребёнка у вершины , мы должны объединить поддерево ребёнка с классом вершины ( — объединить классы вершин и , а наименьшим общим предком представителя нового класса сделать вершину ). Система непересекающихся множеств сама определит представителя в зависимости от используемой нами эвристики. Нам надо лишь правильно установить значение массива у нового представителя.
После того как мы обработали всех детей вершины , мы можем ответить на все запросы вида , где — уже посещённая вершина. Нетрудно заметить, что . Для каждого запроса это условие (что одна вершина уже посещена, а другую мы обрабатываем) выполнится только один раз.
Реализация
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]]
Корректность
Случай, когда является наименьшим общим предком вершин и , обработается правильно, потому что по алгоритму в этот момент .
Пусть теперь наименьшим общим предком вершин и будет вершина, отличная от этих двух. Во время обработки запроса алгоритм точно вернёт общего предка этих двух вершин, так как он будет предком одной из вершин по массиву , а предком другой из-за обхода в глубину.
Покажем, что найдём наименьшего предка. Пусть это не так. Тогда существует какая-то вершина , которая тоже является предком вершин и , и из которой мы вышли раньше во время обхода в глубину. Но тогда ситуация, что одна из вершин посещена, а у другой рассмотрены все дети, должна была выполниться раньше, и в качестве ответа должна была вернуться вершина .
Замечание: для корректности алгоритма достаточно было бы одного массива , а представителем класса всегда выбирать наименьшего общего предка вершин класса. Это несложно сделать, так как мы всегда объединяем ребёнка со своим родителем. Но в таком случае алгоритм получился бы менее эффективным, потому что одна только эвристика сжатия путей работает недостаточно быстро.
Оценка сложности
Она состоит из нескольких частей.
- Обход в глубину выполняется за .
- Операции по объединению множеств, которые в сумме для всех разумных работают времени. Каждый запрос будет рассмотрен дважды — при посещении вершины и , но обработан лишь один раз, поэтому можно считать, что все запросы обработаются суммарно за .
- Для каждого запроса проверка условия и определение результата, опять же, для всех разумных выполняется за .
Следовательно, итоговая асимптотика — , что при достаточно больших составляет на один запрос.
