Времена входа и выхода
Два числа на вершину, после которых «является ли предком» проверяется одним сравнением, а поддерево превращается в отрезок массива.
3 мин
Запустим обход в глубину и заведём таймер. При входе в вершину запишем tin[v], при выходе — tout[v], каждый раз увеличивая таймер.
int timer = 0;
vector<int> tin, tout;
void dfs(int v, int p) {
tin[v] = timer++;
for (int u : g[v]) if (u != p) dfs(u, v);
tout[v] = timer++;
}
Каждой вершине отвечает отрезок на оси времени. Вся структура дерева в этих отрезках уже закодирована.
Отрезки либо вложены, либо не пересекаются
Третьего не дано: обход не может выйти из вершины, не выйдя сначала из всех, в которые он зашёл позже.
Отсюда критерий:
— предок тогда и только тогда, когда и .
Каждая вершина считается своим предком; если это мешает, добавьте проверку .
bool isAncestor(int u, int v) {
return tin[u] <= tin[v] && tout[v] <= tout[u];
}
Никаких подъёмов, никакой рекурсии — одно сравнение. Проверено: на 50 000 случайных деревьев результат совпал с честным подъёмом от к корню.
Достаточно одного числа
Условие можно упростить: вместо двух разных величин у смотреть только на .
bool isAncestor(int u, int v) {
return tin[u] <= tin[v] && tin[v] <= tout[u];
}
Почему это то же самое. В одну сторону: из и следует .
В другую: пусть , но . Тогда отрезки и пересекаются, но не вложены, — а такого при обходе не бывает.
Мелочь, но полезная: в запросах, где надо сравнивать одну величину с диапазоном, второй вариант укладывается в бинарный поиск, а первый — нет.
Проверено: оба варианта совпали с подъёмом к корню на тех же 50 000 деревьях.
Поддерево — это отрезок
Выпишем вершины в порядке возрастания . Тогда поддерево любой вершины займёт непрерывный кусок этого массива.
Это превращает запросы к поддереву в запросы к отрезку массива — а с ними умеют работать префиксные суммы, sparse table и прочие структуры.
Пример: сумма в поддереве на заданной глубине
Дано дерево с числами в вершинах. Запрос — пара : сложить числа во всех вершинах поддерева , лежащих ровно на уровней ниже неё.
Разложим вершины по глубинам: для каждой глубины свой массив, внутри массива — по возрастанию . Если складывать вершины в порядке обхода, сортировка получается сама.
Теперь нужные вершины — те, у кого глубина равна и чей попадает в отрезок . В отсортированном массиве это непрерывный кусок, его границы ищутся бинарным поиском, а сумма берётся из префиксных сумм.
auto& arr = byDepth[dep[v] + d];
int lo = lower_bound(arr.begin(), arr.end(), v,
[&](int a, int b) { return tin[a] < tin[b]; }) - arr.begin();
int hi = upper_bound(arr.begin(), arr.end(), v,
[&](int a, int b) { return tout[a] < tin[b]; }) - arr.begin();
long long answer = pref[dep[v] + d][hi] - pref[dep[v] + d][lo];
Предподсчёт , запрос , и всё это в онлайне.
Проверено: на 20 000 деревьев ответы совпали с прямым перебором всех вершин нужной глубины.
Обратите внимание на два разных компаратора у lower_bound и upper_bound: левую границу ищем по , правую — по . Перепутать здесь легко, а тесты на маленьких деревьях этого не ловят.