Метод двоичного подъёма
Метод двоичного подъема — один из самых простых методов для решения задачи LCA в online. Он не использует метод решение задачи RMQ и основан на методе динамического программирования.
Описание алгоритма
Как и большинство on-line алгоритмов для решения задачи LCA, этот метод делает сначала препроцессинг, чтобы потом отвечать на запросы.
Препроцессинг
Препроцессинг заключается в том, чтобы посчитать функцию:
— номер вершины, в которую мы придем если пройдем из вершины вверх по подвешенному дереву шагов, причем если мы пришли в корень, то мы там и останемся. Для этого сначала обойдем дерево в глубину и для каждой вершины запишем номер ее родителя и глубину вершины в подвешенном дереве . Если — корень, то . Тогда для функции есть рекуррентная формула:
Для того чтобы отвечать на запросы нам нужны будут только те значения
, где , ведь при больших значение будет номером корня.Всего состояний динамики
, где — это количество вершин в дереве. Каждое состояние считается за . Поэтому суммарная сложность времени и памяти препроцессинга — .Ответы на запросы
Ответы на запросы будут происходить за время
. Для ответа на запрос заметим сначала, что если , для некоторых и , то . Поэтому если , то пройдем от вершины на шагов вверх, это и будет новое значение и это можно сделать за . Можно записать число в двоичной системе, это представление этого число в виде суммы степеней двоек, и для всех пройти вверх последовательно из вершины в .Дальше считаем, что
.Если
, то ответ на запрос .А если
, то найдем такие вершины и , такие что , — предок , — предок и . Тогда ответом на запрос будет .Научимся находить эти вершины
и . Для этого сначала инициализируем и . Дальше на каждом шаге находим такое максимальное , что . И проходим из вершин и на шагов вверх. Если такого найти нельзя, то значения и , это те самые вершины, которые нам требуется найти, ведь .Оценим время работы. Заметим, что найденные
строго убывают. Во-первых, потому что мы находим на каждом шаге максимальное значение , а во-вторых, два раза подряд мы одно и то же получить не можем, так как тогда получилось бы, что можно пройти шагов, а значит вместо первого , мы бы нашли . А, значит, всего значений , их можно перебирать в порядке убывания. Сложность ответа на запрос .Псевдокод
function preprocess():
int[] p = dfs(0)
for i = 1 to n
dp[i][0] = p[i]
for j = 1 to log(n)
for i = 1 to n
dp[i][j] = dp[dp[i][j - 1]][j - 1]
int lca(int v, int u):
if d[v] > d[u]
swap(v, u)
for i = log(n) downto 0
if d[u] - d[v]
u = dp[u][i]
if v == u
return v
for i = log(n) downto 0
if dp[v][i] != dp[u][i]
v = dp[v][i]
u = dp[u][i]
return p[v]
Модификация предподсчета за времени и памяти
Воспользуемся идеей Heavy-light декомпозиции, которая разбивает все вершины дерева на вершинно непересекающиеся пути так, что поднимаясь от любой вершины до корня дерева придется сменить не более различных путей.
Препроцессинг
Построим декомпозицию, для каждой вершины, помимо ее предка будем хранить дополнительно следующие значения:
- Расстояние до корня дерева.
- Номер предка. (Начало пути от корня, ведущего в вершину).
- Номер вершины, в которую выходит ребро из предка, ведущее в вершину.
Вычисление LCA
Будем рекурсивно подниматься в направлении корня. Пусть на данной итерации рассматриваем вершины
, . Сравним вершины ( , ), в которые идут ребра из предков рассматриваемых вершин.- = . Или лежит на пути от корня к , или наоборот. За LCA выберем ту вершину, которая лежит ближе к корню.
- . Нужно приблизить одну из вершин к корню, выбрав вместо нее её предка. Приближать будем на основании того, какая из вершин останется дальше от корня, после приближения.
Очевидно, что в результате придем или в одну и ту же вершину, или одна из вершин окажется на пути от корня к другой. Тем самым мы найдем LCA.
Псевдокод
# Находит наименьшего общего предка вершин u и v def lca(int u, int v): # Проверяем вершины, в которые идут ребра из предков. if (turn[u] == turn[v]): # Ответ найден, выберем ближайшую к корню. if (dist[u] < dist[v]): return u else: return v # Приблизимся к корню. Выбираем наиболее удаленную вершину. if (dist[last[u]] > dist[last[v]]): return lca(last[u], v) return lca(last[v], u)
Ассимптотика
- Память: Для реализации алгоритма требуется памяти.
- Препроцессинг: Heavy-light декомпозиция строится за .
- Запросы: По свойству heavy-light декомпозиции, на пути от вершины к корню мы сменим не более путей. Значит время выполнения запроса также .