1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10

# теория · шаг 1 из 10

Наименьший общий предок: задача и наивное решение

Коротко

Наименьший общий предок (LCA) двух вершин дерева - самая глубокая вершина, которая является предком обеих. На нём стоят расстояния в дереве, запросы «лежит ли вершина в поддереве», поиск развилки двух веток в истории версий и многое другое. Наивное решение простое: поднять более глубокую вершину до уровня второй, а дальше поднимать обе одновременно, пока не совпадут - это O(глубины), то есть до O(V) на цепочке. Для одного запроса нормально, для миллиона - нет. Заодно в этом уроке собирается весь нужный инструментарий: глубина, родитель, проверка «предок ли» за O(1) через времена входа и выхода - и формула расстояния dist(u, v) = depth[u] + depth[v] − 2·depth[LCA].

Если ты знаешь определение LCA, умеешь считать расстояние через глубины и понимаешь, почему наивный подъём стоит O(глубины), листай до «Теперь сам».

Как думаешь, где ветки разошлись

Представь историю версий проекта: дерево коммитов, каждый знает своего родителя. Прежде чем читать дальше, ответь: как найти коммит, в котором разошлись две ветки? И сколько шагов это займёт, если ветки разошлись тысячу коммитов назад, а история длиной в миллион?

На что это похоже

Генеалогическое древо и вопрос «кем эти двое друг другу приходятся». Способ, которым это делают люди: поднимаемся от обоих по поколениям, пока не упрёмся в общего предка. Если один потомок младше, сперва подтягиваем его на тот же уровень поколений, а потом идём вверх синхронно. Общий предок, найденный первым, - самый близкий, а не какой-нибудь прапрадед.

Механизм: глубина, родитель, подъём

parent = [0] * V
depth = [0] * V
tin = [0] * V
tout = [0] * V
timer = 0

def dfs(v, p):
    global timer
    parent[v] = p
    tin[v] = timer; timer += 1
    for u in adj[v]:            # обычный список смежности, как во всём разделе
        if u != p:              # родителя пропускаем - иначе уйдём обратно
            depth[u] = depth[v] + 1
            dfs(u, v)
    tout[v] = timer; timer += 1

# У КОРНЯ родителем считаем его самого - это соглашение понадобится в следующем
# уроке (в таблице подъёмов -1 читался бы как индекс последнего элемента).
depth[root] = 0
dfs(root, root)

def is_ancestor(u, v):                 # из главы про обход: O(1)
    return tin[u] <= tin[v] and tout[v] <= tout[u]

def lca_naive(u, v):
    while depth[u] > depth[v]:         # подтянули глубокую вершину
        u = parent[u]
    while depth[v] > depth[u]:
        v = parent[v]
    while u != v:                      # дальше поднимаем обе
        u = parent[u]
        v = parent[v]
    return u

Две формулы, ради которых LCA чаще всего и считают:

расстояние в дереве:  dist(u, v) = depth[u] + depth[v] − 2·depth[ LCA(u, v) ]
лежит ли v в поддереве u:  tin[u] <= tin[v] и tout[v] <= tout[u]     ← O(1)

Первая работает потому, что путь между вершинами в дереве единственный и проходит ровно через их наименьшего общего предка: поднимаемся из u до него (depth[u] − depth[LCA] шагов) и спускаемся к v (ещё depth[v] − depth[LCA]).

rpc abd u LCA(u, b) = p - обведена LCA(u, d) = r: ветви разошлись в самом корне dist(u, b) = 3 + 2 − 2·1 = 3
Наименьший общий предок - место, где сходятся пути двух вершин наверх. Через него же считается расстояние между ними.

Правило

LCA(u, v) - самая глубокая вершина, являющаяся предком и u, и v (вершина считается предком самой себя). Наивный алгоритм: подтянуть более глубокую вершину до уровня второй по родительским ссылкам, затем поднимать обе синхронно до совпадения - O(глубины), в худшем случае O(V). Расстояние в дереве: dist(u, v) = depth[u] + depth[v] − 2·depth[LCA(u, v)]. Проверка «является ли u предком v» - O(1) через времена входа и выхода.

Разбор: считаем LCA и расстояние руками

Дерево: корень r; дети r - p и c; дети p - a и b; ребёнок a - u; ребёнок c - d. Глубины: r = 0, p = c = 1, a = b = d = 2, u = 3.

LCA(u, b). Глубины 3 и 2 - подтягиваем u: u → a, теперь обе на глубине 2 (a и b). Не совпали - поднимаем обе: a → p, b → p. Совпали: LCA = p. Расстояние: depth[u] + depth[b] − 2·depth[p] = 3 + 2 − 2·1 = 3. Проверим по картинке: u → a → p → b - три ребра ✓.

LCA(u, d). Глубины 3 и 2 - подтягиваем u до a. Поднимаем обе: a → p, d → c. Не совпали. Ещё раз: p → r, c → r. Совпали: LCA = r. Расстояние: 3 + 2 − 2·0 = 5: u → a → p → r → c → d - пять рёбер ✓.

LCA(a, u). Здесь важный частный случай: a - предок u. Подтягиваем u с глубины 3 до глубины 2 - получаем a, и цикл синхронного подъёма не делает ни одного шага, потому что вершины уже совпали. LCA = a - и это правильный ответ: вершина считается предком самой себя.

Разбор: почему наивный способ иногда невыносим

Цена одного запроса - O(глубины). В сбалансированном дереве глубина log V, и миллион запросов к дереву из миллиона вершин обойдётся в 20·10⁶ шагов - терпимо. Но дерево не обязано быть сбалансированным: история версий, дерево каталогов, цепочка комментариев, дерево разбораРазбирается в разделе 21, глава «Контекстно-свободные языки и магазинные автоматы»: Грамматика: правила, которые порождают язык, и дерево разбора длинного выражения легко вытягиваются в почти линейную цепочку. Тогда глубина V, и миллион запросов - это 10¹² операций, то есть часы.

Отсюда постановка задачи, которой занят остаток главы: сделать ПРЕДПОДСЧЁТ, после которого каждый запрос стоит O(log V) или даже O(1). Ключевое наблюдение, ведущее к решению: наивный подъём шагает по одному родителю за раз, а это то же самое, что искать число, прибавляя единицу. Если бы мы умели прыгать сразу на 1, 2, 4, 8, ... предков, любой подъём собирался бы из log V прыжков - ровно как любое число собирается из степеней двойки. Это и есть двоичные подъёмы из следующего урока.

Что ломается без этого

Первое: забыть подтянуть вершины к одной глубине и начать поднимать сразу обе - они разминутся и никогда не совпадут (либо совпадут случайно, дав неверный ответ). Второе: считать, что вершина не является предком самой себя - тогда LCA(a, u) для предка и потомка вернёт родителя a, и все расстояния сдвинутся. Третье: применять LCA к графу, который не дерево, - понятие определено для деревьев (и для ДАГов оно другое, там общих предков может быть несколько несравнимых). Четвёртое: хранить глубину как число рёбер в одном месте и как число вершин в другом - формула расстояния сразу начнёт врать на единицу; глубина корня должна быть нулём. Пятое: рекурсивный обход для построения parent и depth на цепочке из миллиона вершин - привет переполнение стека, о котором была отдельная речь.

Зачем это в работе

LCA - это «где разошлись» и «как далеко друг от друга». Системы контроля версий ищут базу слияния двух веток - буквально LCA в дереве коммитов (в общем случае это ДАГ, и там сложнее, но идея та же). Файловые системы и права доступа: общий родительский каталог двух путей. Иерархии организаций и категорий: ближайшая общая категория двух товаров, общий руководитель двух сотрудников. В вычислительной геометрии и биоинформатике LCA в суффиксных деревьяхРазбирается в разделе 17, глава «Суффиксный массив, LCP и суффиксное дерево»: Суффиксное дерево: сжатый бор всех суффиксов даёт длину наибольшего общего префикса двух подстрок. Плюс целое семейство задач на деревьях, где ответ считается как «сумма по пути» и раскладывается через предка: f(u, v) = f(корень, u) + f(корень, v) − 2·f(корень, LCA) - тот же приём, что и с расстоянием.

Теперь сам

Дерево: корень 1; дети 1 - вершины 2 и 3; дети 2 - вершины 4 и 5; ребёнок 4 - вершина 6; ребёнок 3 - вершина 7. Найди LCA(6, 5), LCA(6, 7) и расстояния между этими парами.

Ответ. Глубины: 1 → 0, 2 → 1, 3 → 1, 4 → 2, 5 → 2, 7 → 2, 6 → 3. LCA(6, 5): глубины 3 и 2, подтягиваем шестёрку: 6 → 4. Теперь обе на глубине 2: 4 и 5, не совпадают - поднимаем обе: 4 → 2, 5 → 2. Совпали: LCA = 2. Расстояние: depth[6] + depth[5] − 2·depth[2] = 3 + 2 − 2·1 = 3 (путь 6 → 4 → 2 → 5, три ребра ✓). LCA(6, 7): глубины 3 и 2, подтягиваем: 6 → 4. Поднимаем обе: 4 → 2, 7 → 3. Не совпали. Ещё раз: 2 → 1, 3 → 1. Совпали: LCA = 1. Расстояние: 3 + 2 − 2·0 = 5 (путь 6 → 4 → 2 → 1 → 3 → 7, пять рёбер ✓). Обрати внимание на цену: во втором случае подъём дошёл до самого корня - именно так наивный способ и ведёт себя в худшем случае, когда вершины лежат в разных ветвях у самого верха.

Главное

Наименьший общий предок двух вершин дерева - самая глубокая вершина, являющаяся предком обеих (вершина считается предком самой себя - это важный частный случай). Наивный алгоритм: подтянуть более глубокую вершину к уровню второй по родительским ссылкам, затем поднимать обе синхронно до совпадения; цена O(глубины) - приемлемо для одиночных запросов и невыносимо для массовых, потому что реальные деревья (истории версий, каталоги, цепочки комментариев) легко вытягиваются в почти линейную цепочку. Две формулы, ради которых LCA и считают: dist(u, v) = depth[u] + depth[v] − 2·depth[LCA] (путь в дереве единственный и проходит через предка) и проверка «лежит ли v в поддереве u» за O(1) через времена входа и выхода. Наивный подъём шагает по одному родителю - как счёт прибавлением единицы; если научиться прыгать на 1, 2, 4, 8 предков сразу, любой подъём соберётся из log V прыжков. Это и есть следующий урок.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.