Дерево — связный граф без циклов на n вершинах и n−1 рёбрах.
Между любыми двумя вершинами ровно один путь
Удаление любого ребра разбивает граф на две компоненты
Можно выбрать корень → появляется иерархия (родитель, потомки, поддерево)
Граф с циклом:
1 — 2 — 3
| |
4 — 5 — 6
Дерево:
1
/ \
2 3
/ \ \
4 5 6
Деревья проще графов: нет циклов, есть единственные пути. Это позволяет использовать специальные структуры данных.
Задача: e-olymp 11429 — Подчинённые
В компании n сотрудников. Структура подчинения — дерево: у каждого (кроме директора) ровно один начальник. Для каждого сотрудника определить, сколько у него подчинённых (прямых и непрямых).
n ≤ 2·105. Лимит: 1 сек.
1
/ \
2 3
/ \
4 5
Ответ: сотрудник 1 → 4, сотрудник 2 → 2, сотрудники 3, 4, 5 → 0
Как бы вы решили эту задачу?
DFS на дереве — без visited[]
В дереве нет циклов → DFS не может вернуться в уже посещённую вершину (кроме родителя)
Вместо visited[] достаточно передавать параметр parent
DFS из корня обходит каждую вершину ровно раз и задаёт естественный порядок обхода
void dfs(int v, int parent) {
for (int u : adj[v]) {
if (u != parent) {
dfs(u, v);
}
}
}
DFS на дереве задаёт линейный порядок обхода вершин. Этот порядок — основа всего, что мы будем делать сегодня.
Укоренение дерева: parent[] и depth[]
Задача: дано дерево, корень = 1. Для каждой вершины найти parent[v] и depth[v].
vector<int> adj[MAXN];
int parent[MAXN], depth[MAXN];
void dfs(int v, int p, int d) {
parent[v] = p;
depth[v] = d;
for (int u : adj[v]) {
if (u != p) {
dfs(u, v, d + 1);
}
}
}
// Вызов: dfs(1, 0, 0);
depth 0: 1
depth 1: / \
2 3
depth 2: / \ \
4 5 6
Размер поддерева через DFS
sz[v] = количество вершин в поддереве v (включая саму v)
sz[v] = 1 + Σ sz[u] для всех детей u вершины v
Ответ на задачу: подчинённых у v = sz[v] − 1
int sz[MAXN];
void dfs(int v, int p) {
sz[v] = 1;
for (int u : adj[v]) {
if (u != p) {
dfs(u, v);
sz[v] += sz[u];
}
}
}
Поддерево = непрерывный отрезок в DFS-порядке. Это позволяет свести задачи на поддеревьях к задачам на отрезках массива!
Задача: проверка предка за O(1)
Дано корневое дерево из n вершин. Отвечайте на q запросов: «является ли вершина u предком вершины v?»
n, q ≤ 2·105. Нужен O(1) на запрос.
Наивно: подняться от v к корню, проверить, встретим ли u → O(n) на запрос → TLE
Идея: если поддерево u — отрезок, то v ∈ поддерево(u) ⇔ v попадает в этот отрезок
Нам нужны два числа для каждой вершины
Чтобы задать отрезок [начало, конец] для поддерева v, нужно знать:
Когда DFS вошёл в вершину v → tin[v]
Когда DFS вышел из вершины v → tout[v]
Тогда: поддерево v = все вершины u, где tin[v] ≤ tin[u] ≤ tout[v]
Euler tour — не новый алгоритм. Это просто DFS, в котором мы записываем время входа и выхода.
Euler Tour: tin[] и tout[]
int timer = 0;
int tin[MAXN], tout[MAXN];
void dfs(int v, int p) {
tin[v] = timer++;
for (int u : adj[v]) {
if (u != p) {
dfs(u, v);
}
}
tout[v] = timer - 1;
}
Мы используем: tout[v] = timer - 1 (закрытый отрезок)
Поддерево v = отрезок [tin[v], tout[v]]
Будьте внимательны с соглашением! В разных источниках tout определяется по-разному. Мы используем: tout[v] = номер последней вершины, посещённой в поддереве v.
u является предком v тогда и только тогда, когда: tin[u] ≤ tin[v] И tout[v] ≤ tout[u]
(Вершина v «внутри» временного отрезка вершины u)
Примеры (из предыдущего слайда):
Является ли 2 предком 5? tin[2]=1 ≤ tin[5]=2 ✓ и tout[5]=2 ≤ tout[2]=3 ✓ → ДА
Является ли 2 предком 7? tin[2]=1 ≤ tin[7]=6 ✓ но tout[7]=6 ≤ tout[2]=3? НЕТ
bool is_ancestor(int u, int v) {
return tin[u] <= tin[v] && tout[v] <= tout[u];
}
Главная идея: поддерево → отрезок
Пусть order[] — массив вершин в порядке tin. Тогда:
Поддерево вершины v = order[tin[v] .. tout[v]]
Это непрерывный подмассив длины sz[v]
Следствие — любая задача на поддереве сводится к задаче на отрезке массива:
Сумма значений в поддереве → сумма на отрезке
Минимум в поддереве → минимум на отрезке
Обновление значений в поддереве → обновление на отрезке
Euler tour превращает дерево в массив. Поддерево = отрезок. Все инструменты для массивов (prefix sum, segment tree, BIT) теперь работают на деревьях!
Задача: сумма значений в поддереве
Дано дерево из n вершин, каждая вершина v имеет вес w[v]. Для каждой вершины v найти сумму весов всех вершин в её поддереве.
Решение:
Запускаем DFS, вычисляем tin[v]
Создаём массив a[] в Euler-порядке: a[tin[v]] = w[v]
Считаем префиксные суммы
Сумма в поддереве v = prefix[tout[v]+1] - prefix[tin[v]]
// После DFS (tin[], tout[] заполнены)
vector<long long> a(n), prefix(n + 1, 0);
for (int v = 1; v <= n; v++)
a[tin[v]] = w[v];
for (int i = 0; i < n; i++)
prefix[i + 1] = prefix[i] + a[i];
// Сумма в поддереве v:
long long subtree_sum = prefix[tout[v] + 1] - prefix[tin[v]];
Сложность: O(n) предвычисление, O(1) на запрос
Euler Tour: полная реализация
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
vector<int> adj[MAXN];
int tin[MAXN], tout[MAXN], sz[MAXN], depth[MAXN], parent[MAXN];
int timer = 0;
int order[MAXN]; // order[i] = вершина с tin = i
void dfs(int v, int p, int d) {
parent[v] = p;
depth[v] = d;
tin[v] = timer;
order[timer] = v;
timer++;
sz[v] = 1;
for (int u : adj[v]) {
if (u != p) {
dfs(u, v, d + 1);
sz[v] += sz[u];
}
}
tout[v] = timer - 1;
}
int main() {
int n;
cin >> n;
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
adj[u].push_back(v);
adj[v].push_back(u);
}
dfs(1, 0, 0);
// Поддерево v = отрезок [tin[v], tout[v]]
// sz[v] == tout[v] - tin[v] + 1
return 0;
}
Проверка: sz[v] == tout[v] - tin[v] + 1
Осторожно: глубина рекурсии
Проблема: при n = 200000 и дереве-«бамбуке» глубина рекурсии = n
Стандартный стек = 1–8 МБ → segmentation fault
Решения:
Увеличить стек (Linux: ulimit -s unlimited)
Итеративный DFS (сложнее, но возможно)
На IOI: обычно стека хватает (256MB stack)
На Codeforces добавляйте в начало main(): #pragma comment(linker, "/STACK:1000000000") (Windows)
или используйте итеративный DFS. На IOI — стека обычно хватает.
Практика: 20 минут
★ e-olymp 11429 «Подчинённые» — посчитать количество подчинённых (sz[])
★★ CSES 1137 «Subtree Queries» — сумма в поддереве с обновлениями (Euler tour + BIT)
★★★ CSES 1138 «Path Queries» — сумма на пути до корня (Euler tour + BIT, разностный трюк)
Подсказка к CSES 1137: Обновление вершины v = обновление позиции tin[v]. Запрос суммы в поддереве v = сумма на отрезке [tin[v], tout[v]].
Перерыв после практики: 15:45–16:00
LCA — Lowest Common Ancestor
LCA(u, v) — наименьший (самый глубокий) общий предок вершин u и v в корневом дереве.
Другими словами: вершина w, которая является предком и u, и v, и имеет максимальную глубину.
1
/ \
2 3
/ \ \
4 5 6
/
7
LCA(7, 5) = 2
LCA(7, 6) = 1
LCA(4, 5) = 2
LCA(4, 7) = 4 (сама вершина!)
LCA(u, v) может быть равен u или v! (Если один из них — предок другого)
Зачем нужен LCA?
Расстояние между вершинами: dist(u, v) = depth[u] + depth[v] − 2·depth[LCA(u, v)]
Путь между вершинами: путь u→v проходит через LCA(u, v)
Запросы на путях: сумма/минимум на пути = объединение двух вертикальных путей через LCA
Продвинутые структуры: HLD, virtual tree, auxiliary tree
LCA сводит задачи на путях в дереве к задачам на вертикальных путях (от вершины к предку). А вертикальные пути — это просто отрезки на пути к корню.
Задача: e-olymp 3298 — Общий предок
Дано корневое дерево из n вершин. Ответьте на q запросов вида «найти LCA(u, v)».
n ≤ 2·105, q ≤ 2·105. Лимит: 2 сек.
Предобработка за O(n log n) или лучше
Каждый запрос за O(log n) или лучше
Начнём с наивного алгоритма, потом ускорим.
Наивный LCA: выравнивание + подъём
Выравнивание глубин: если depth[u] > depth[v], поднимаем u вверх
Одновременный подъём: поднимаем u и v по одному ребру вверх, пока u ≠ v
int lca_naive(int u, int v) {
// Шаг 1: выравниваем глубины
while (depth[u] > depth[v]) u = parent[u];
while (depth[v] > depth[u]) v = parent[v];
// Шаг 2: поднимаемся вместе
while (u != v) {
u = parent[u];
v = parent[v];
}
return u;
}
Сложность: O(n) в худшем случае (дерево-бамбук)
При q = 200000 запросов → O(n·q) = O(4·1010) — TLE
Идея: прыгать не по 1, а по 2k
Любое число можно представить как сумму степеней двойки.
Пример: 13 = 8 + 4 + 1 = 23 + 22 + 20
Значит: подняться на 13 шагов = подняться на 8 + 4 + 1 шагов
Если мы заранее знаем «предок на расстоянии 2k» для каждой вершины:
Подняться на h шагов = O(log h) прыжков
Каждый прыжок — O(1) (таблица предвычислена)
Вместо O(n) шагов по 1 ребру — O(log n) прыжков по 2k рёбер.
Нужно лишь предвычислить таблицу up[v][k] = предок v на расстоянии 2k.
Binary Lifting: таблица up[v][k]
up[v][k] = предок вершины v на расстоянии 2k
База:up[v][0] = parent[v] (предок на расстоянии 1)
Рекуррентность:up[v][k] = up[ up[v][k-1] ][k-1]
Подняться на 2k = подняться на 2k-1, потом ещё на 2k-1
up[v][2] = предок на 4:
v --------→ up[v][1] --------→ up[v][2] = up[up[v][1]][1]
2^1=2 2^1=2
Размер таблицы: n × LOG, где LOG = ⌈log₂n⌉ ≈ 18 для n = 200000
Предвычисление up[][]
const int LOG = 18; // ceil(log2(200000)) = 18
int up[MAXN][LOG];
void preprocess(int root) {
// Сначала DFS для parent[] и depth[]
dfs(root, 0, 0);
// Заполняем таблицу
for (int v = 1; v <= n; v++)
up[v][0] = parent[v];
for (int k = 1; k < LOG; k++)
for (int v = 1; v <= n; v++)
up[v][k] = up[ up[v][k-1] ][k-1];
}
Внешний цикл по k — по уровню (от маленьких к большим)
Порядок важен: для k нужны значения k−1, которые уже вычислены
Соглашение: up[root][k] = 0 для всех k (вышли за корень)
Сложность: O(n log n) по времени и памяти
Пример таблицы up[][]
1
/ \
2 3
/ \
4 5
/
6
v
depth
up[v][0]
up[v][1]
up[v][2]
1
0
0
0
0
2
1
1
0
0
3
1
1
0
0
4
2
2
1
0
5
2
2
1
0
6
3
4
2
0
Разбор up[6][1]: up[up[6][0]][0] = up[4][0] = 2. Из 6 на 2 шага: 6→4→2 ✓
Разбор up[6][2]: up[up[6][1]][1] = up[2][1] = 0. Из 6 на 4 шага: выход за корень → 0
LCA, Шаг 1: выравнивание глубин
Привести u и v к одной глубине: разложить diff в двоичную систему
// Шаг 1: выравнивание
if (depth[u] < depth[v]) swap(u, v);
int diff = depth[u] - depth[v];
for (int k = 0; k < LOG; k++) {
if ((diff >> k) & 1) {
u = up[u][k];
}
}
// Теперь depth[u] == depth[v]
Пример 1: diff = 4 = 100₂ → прыжок k=2: u = up[u][2] (на 4 уровня) ✓
После шага 1: depth[u] == depth[v]. Если u == v — готово. Иначе:
Перебираем k от большого к малому.
Если up[u][k] ≠ up[v][k] — прыгаем (остаёмся ниже LCA).
Если up[u][k] == up[v][k] — не прыгаем (иначе перепрыгнем!).
После всех прыжков: u и v — дети LCA. Ответ: parent[u].
k=2: up[u][2]≠up[v][2] → прыгаем (поднялись на 4)
k=1: up[u][1]==up[v][1] → НЕ прыгаем (перепрыгнем!)
k=0: up[u][0]≠up[v][0] → прыгаем (поднялись на 1)
Итого: u и v — дети LCA. Ответ = parent[u].
Пример: LCA(7, 5)
1
/ \
2 3
/|\ \
4 5 8 6
/ \
7 9
Шаг 1: depth[7]=3, depth[5]=2. diff=1. Прыжок k=0: u = up[7][0] = 4. Теперь u=4, v=5.
Проверка: u=4 ≠ v=5 → шаг 2
Шаг 2:
k=3: up[4][3]=0, up[5][3]=0 → равны, не прыгаем
k=2: up[4][2]=0, up[5][2]=0 → равны, не прыгаем
k=1: up[4][1]=1, up[5][1]=1 → равны, не прыгаем
k=0: up[4][0]=2, up[5][0]=2 → равны, не прыгаем
Ответ: parent[4] = parent[5] = 2 ✓
Пример: LCA(9, 5) — с реальными прыжками
Дерево со слайда 7.6. parent: 9→8, 8→3, 3→1, 5→2, 2→1
Шаг 1: depth[9]=3, depth[5]=2. diff=1. u = up[9][0] = 8. Теперь u=8, v=5, depth=2.
Проверка: u=8 ≠ v=5 → шаг 2
Шаг 2:
k=3: up[8][3]=0, up[5][3]=0 → равны, не прыгаем
k=2: up[8][2]=0, up[5][2]=0 → равны, не прыгаем
k=1: up[8][1]=1, up[5][1]=1 → равны, не прыгаем
k=0: up[8][0]=3, up[5][0]=2 → не равны! Прыгаем!
u = up[8][0] = 3, v = up[5][0] = 2. Теперь u=3, v=2 (глубина 1)
Ответ: parent[3] = parent[2] = 1 ✓
Проверяйте: после шага 2, parent[u] == parent[v] == LCA.
Полный код: LCA за O(log n)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
const int LOG = 18;
vector<int> adj[MAXN];
int depth[MAXN], up[MAXN][LOG];
int n;
void dfs(int v, int p, int d) {
depth[v] = d;
up[v][0] = p;
for (int k = 1; k < LOG; k++)
up[v][k] = up[ up[v][k-1] ][k-1];
for (int u : adj[v]) {
if (u != p)
dfs(u, v, d + 1);
}
}
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
int diff = depth[u] - depth[v];
for (int k = 0; k < LOG; k++)
if ((diff >> k) & 1)
u = up[u][k];
if (u == v) return u;
for (int k = LOG - 1; k >= 0; k--) {
if (up[u][k] != up[v][k]) {
u = up[u][k];
v = up[v][k];
}
}
return up[u][0];
}
int main() {
int q;
cin >> n >> q;
for (int i = 0; i < n - 1; i++) {
int a, b;
cin >> a >> b;
adj[a].push_back(b);
adj[b].push_back(a);
}
dfs(1, 0, 0);
while (q--) {
int u, v;
cin >> u >> v;
cout << lca(u, v) << "\n";
}
return 0;
}
Сложность: O(n log n) предобработка, O(log n) на запрос
Сложность Binary Lifting
Этап
Время
Память
Предобработка (DFS + таблица)
O(n log n)
O(n log n)
Один запрос LCA
O(log n)
—
Всего для q запросов
O(n log n + q log n)
O(n log n)
Для n = q = 200000, LOG = 18: предобработка ~3.6M операций, память ~14 МБ
Метод
Предобработка
Запрос
Память
Наивный
O(n)
O(n)
O(n)
Binary Lifting
O(n log n)
O(log n)
O(n log n)
Euler tour + RMQ
O(n log n)
O(1)
O(n log n)
Farach-Colton & Bender
O(n)
O(1)
O(n)
Binary Lifting — лучший баланс простоты и эффективности для олимпиад. Запомните этот шаблон!
Подсказки:
• CSES 1135: dist(u,v) = depth[u] + depth[v] − 2·depth[LCA(u,v)]
• CF 191C: +1 к u и v, −2 из LCA(u,v). Ответ для ребра = сумма в поддереве нижнего конца.
Последний час — решаем и обсуждаем. Задавайте вопросы!