Дерево называется корневым, если оно ориентированно, и из какой-то вершины (называемой корнем) можно попасть во все остальные.
Примеры корневых деревьев:
- наследование классов в языках программирования (если множественное наследование запрещено),
- дерево факторизации числа на простые (в общем случае не уникальное),
- иерархия в какой-нибудь организации,
- дерево парсинга математичеких выражений.
Задачи на корневые деревья весьма бесполезны в реальной жизни, но зато очень интересны с алгоритмической точки зрения, и поэтому часто встречаются на олимпиадах по программированию.
Посчитаем для каждой вершины времена входа (
vector<int> g[maxn];
int p[maxn], tin[maxn], tout[maxn];
int t = 0;
void dfs (int v) {
tin[v] = t++;
for (int u : g[v])
dfs(u);
tout[v] = t; // иногда счётчик тут тоже увеличивают
}
У этих массивов много полезных свойств:
- Вершина
$u$ является предком$v$ $\iff tin_v \in [tin_u, tout_u)$ . Эту проверку можно делать за константу. - Два полуинтервала —
$[tin_v, tout_v)$ и$[tin_u, tout_u)$ — либо не пересекаются, либо один вложен в другой. - В массиве
$tin$ есть все числа из промежутка от 0 до$n-1$ , причём у каждой вершины свой номер. - Размер поддерева вершины
$v$ (включая саму вершину) равен$tout_v - tin_v$ . - Если ввести нумерацию вершин, соответствующую
$tin$ -ам, то индексы любого поддерева всегда будут каким-то промежутком в этой нумерации.
Последнее свойство на самом деле очень важное. Его можно использовать для обработки разных запросов на поддеревьях, сводя их к запросам на подотрезках, которые уже можно решать стандартными методами — например, через дерево отрезков.
Пример задачи:
Есть корневое дерево. Рядом с каждой вершиной записано число. Поступают два типа запросов: прибавить ко всем вершинам на каком-то поддереве число
$x_i$ и найти значение числа у вершины$v_i$ .
Выпишем все числа у вершин в один массив, в позиции, соответствующие их
Дано корневое дерево. Требуется отвечать на запросы нахождения
$d_i$ -того предка вершины$v_i$ , то есть вершины-предка, находящейся на расстоянии$d_i$ от$v_i$ .
Создадим
Теперь заметим, что внутри одного вектора все отрезки поддеревьев вершин —
Также существует другой способ, требующий
Для большого класса задач требуется решить следующую вспомогательную:
Дано корневое дерево. Требуется отвечать на запросы нахождения наименьшего общего предка вершин
$u_i$ и$v_i$ , то есть вершины$w$ , которая лежит на пути от корня до$u_i$ , на пути от корня до$v_i$ , и при этом самую глубокую (нижнюю) из всех таких.
По-английский эта задача называется Least Common Ancestor. Есть много разных способов её решать, и мы рассмотрим только основные.
Для лучшего понимания — медленно (за линейное время) наименьшего общего предка можно искать так:
bool a (int u, int v) {
return tin[u] <= tin[v] && tin[v] < tout[u];
}
int lca (int u, int v) {
while (!ancestor(u, v))
u = p[u];
return u;
}
Предпосчитаем для каждой вершины её 1-го предка, 2-го предка, 4-го и так далее. Сохраним их в двумерном массиве up
размера up[v][d]
будет храниться предок вершины
Такой препроцессинг можно выполнить за
int up[maxn][logn];
void dfs (int v) {
for (int l = 1; l < logn; l++)
up[v][l] = up[up[v][l-1]][l-1];
tin[v] = t++;
for (int u : g[v]) {
up[u][0] = v;
dfs(u);
}
tout[v] = t;
}
Пусть теперь поступил запрос нахождения LCA — пара вершин
- Проверим, не является ли одна вершина предком другой — в таком случае она и является результатом.
- Иначе, пользуясь массивом
up
, будем подниматься по предкам одной из них, пока не найдём самую высокую вершину, которая ещё не является предком другой. Следующая за ней будет искомым LCA.
Подробнее про второй пункт. Присвоим up[v][i]
не перестанет быть предком
int lca (int v, int u) {
if (a(v, u)) return v;
if (a(u, v)) return u;
for (int l = logn-1; l >= 0; l--)
if (!ancestor(up[v][l], u))
v = up[v][l];
return up[v][0];
}
Указатель up[v][i]
изначально явдяеься корнем дерева, а затем будет каждую итерацию спускаться на
Асимптотика. Препроцессинг занимает up
, и каждый его элемент вычисляется за константу. Ответ на произвольный запрос будет работать за
Пусть нас вместо LCA спрашивают, например, о минимуме на произвольном пути (на всех рёбрах записаны какие-то числа).
Мы можем сделать такой же предподсчет, как в методе двоичных подъемов, но хранить вместе с номером
Заметим, что минимум на пути от
int get_min (int v, int u) {
int res = inf;
for (int l = logn-1; l >= 0; l--)
if (!ancestor(up[v][l], u))
v = up[v][l], res = min(res, mn[v][l]);
for (int l = logn-1; l >= 0; l--)
if (!ancestor(up[u][l], v))
u = up[u][l], res = min(res, mn[u][l]);
return min({res, mn[v][0], mn[u][0]})
}
Аналогичным образом можно считать сумму, gcd
, полиномиальный хэш и много других странных функций на пути, но только в статическом случае (когда у нас нет обновлений). Для динамического случая существует весьма сложный метод, называемый heavy-light декомпозицией.
Подойдём к задаче нахождения наименьшего общего предка с другой стороны.
Пройдёмся по дереву dfs-ом и выпишем два массива: глубины вершин и номера вершин. Записывать мы их будем как когда при входе в вершину, так и при выходе.
Пусть теперь поступил запрос: найти LCA вершин
Получается, что чтобы найти LCA, можно найти позицию минимума на отрезке
На практике асимптотику мы особо не улучшили — пока что всё равно требуется
Асимптотику времени запроса можно улучшить, используя тот факт, что мы на самом деле решаем задачу static RMQ, то есть у нас нет изменений этого массива. Для этого есть более подходящая структура — разреженная таблица. Она позволяет отвечать на запрос минимума за
Примечание: алгоритм нахождения RMQ, описываемый в этой секции, абсолютно бесполезен на практике, однако очень интересен с теоретической точки зрения.
Оказывается, что и LCA, и static RMQ можно считать за
На самом деле, мы решаем не совсем полноценную задачу RMQ. Мы работаем не со всеми массивами целых чисел от 1 до
Сделаем следующее: раз каждые два элемента отличаются на единицу, то сопоставим исходному массиву глубин булевый массив размера
Первая часть предподсчёта. Возьмем константу
Всего блоков таких блоков
Вторая часть предподсчёта. Посчитаем для каждой возможной маски подъёмов / спусков размера
Возможных масок получается немного — ради этого мы и делили логарифм на два при определении
Запрос. Нам нужно с помощью посчитанных структур найти RMQ на каком-то отрезке
-
Для блочной части мы можем просто сделать запрос в sparse table — он будет работать за константу.
-
Для обеих не-блочных частей посчитаем ещё по кандидату на ответ. Для этого нужно прибавить к граничному элементу предподсчитанное значение минимума на маске оставшихся неблочных элементов — её можно за константу получить битовыми операциями над булевым массивом.
Просто посчитать все суффиксные и префиксные минимумы для всех блоков, чтобы обрабатывать второй случай, к сожалению, нельзя — есть один частный случай, когда запрос маленький и не накрывает никакой блок целиком. В данном случае нужно просто взять граничный элемент и прибавить к нему минимум от нужной маски из массива подъёмов.
Наоборот. Этот алгоритм очень важен с теоретической точки зрения, потому что позволяет решать, не только LCA, но и в общем случае static RMQ за линейное время.
Построим декартово дерево, в котором в качестве ключей
Чуть более подробно и с реализацией (автор это никогда не кодил и вам не советует) можно почитать у Емакса. Впрочем, на практике этот алгоритм использовать нецелесообразно из-за большой константы: слишком много чего нужно считать, чтобы избавиться от логарифма в асимптотике.