EduBrick

Времена входа и выхода

Два числа на вершину, после которых «является ли предком» проверяется одним сравнением, а поддерево превращается в отрезок массива.

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++;
}

Каждой вершине отвечает отрезок [tinv,toutv][tin_v, tout_v] на оси времени. Вся структура дерева в этих отрезках уже закодирована.

Отрезки либо вложены, либо не пересекаются

Третьего не дано: обход не может выйти из вершины, не выйдя сначала из всех, в которые он зашёл позже.

Отсюда критерий:

uu — предок vv тогда и только тогда, когда tinutinvtin_u \le tin_v и toutvtoututout_v \le tout_u.

Каждая вершина считается своим предком; если это мешает, добавьте проверку uvu \ne v.

bool isAncestor(int u, int v) {
    return tin[u] <= tin[v] && tout[v] <= tout[u];
}

Никаких подъёмов, никакой рекурсии — одно сравнение. Проверено: на 50 000 случайных деревьев результат совпал с честным подъёмом от vv к корню.

Достаточно одного числа

Условие можно упростить: вместо двух разных величин у vv смотреть только на tinvtin_v.

bool isAncestor(int u, int v) {
    return tin[u] <= tin[v] && tin[v] <= tout[u];
}

Почему это то же самое. В одну сторону: из toutvtoututout_v \le tout_u и tinvtoutvtin_v \le tout_v следует tinvtoututin_v \le tout_u.

В другую: пусть tinutinvtoututin_u \le tin_v \le tout_u, но toutv>toututout_v > tout_u. Тогда отрезки [tinu,toutu][tin_u, tout_u] и [tinv,toutv][tin_v, tout_v] пересекаются, но не вложены, — а такого при обходе не бывает.

Мелочь, но полезная: в запросах, где надо сравнивать одну величину с диапазоном, второй вариант укладывается в бинарный поиск, а первый — нет.

Проверено: оба варианта совпали с подъёмом к корню на тех же 50 000 деревьях.

Поддерево — это отрезок

Выпишем вершины в порядке возрастания tintin. Тогда поддерево любой вершины займёт непрерывный кусок этого массива.

Это превращает запросы к поддереву в запросы к отрезку массива — а с ними умеют работать префиксные суммы, sparse table и прочие структуры.

Пример: сумма в поддереве на заданной глубине

Дано дерево с числами в вершинах. Запрос — пара (v,d)(v, d): сложить числа во всех вершинах поддерева vv, лежащих ровно на dd уровней ниже неё.

Разложим вершины по глубинам: для каждой глубины свой массив, внутри массива — по возрастанию tintin. Если складывать вершины в порядке обхода, сортировка получается сама.

Теперь нужные вершины — те, у кого глубина равна dep(v)+d\text{dep}(v) + d и чей tintin попадает в отрезок [tinv,toutv][tin_v, tout_v]. В отсортированном массиве это непрерывный кусок, его границы ищутся бинарным поиском, а сумма берётся из префиксных сумм.

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];

Предподсчёт O(n)O(n), запрос O(logn)O(\log n), и всё это в онлайне.

Проверено: на 20 000 деревьев ответы совпали с прямым перебором всех вершин нужной глубины.

Обратите внимание на два разных компаратора у lower_bound и upper_bound: левую границу ищем по tintin, правую — по touttout. Перепутать здесь легко, а тесты на маленьких деревьях этого не ловят.