HL-декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.
Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины
Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Докажем несколько полезных свойств такого разбиения.
Утверждение. Дерево разбивается на непересекающиеся пути из тяжелых рёбер.
Доказательство. В каждую вершину входит не более одного тяжелого ребра, и из каждой вершины исходит не более одного тяжелого ребра.
Назовём блоком либо лёгкое ребро, либо вертикальный путь из тяжелых рёбер.
Утверждение. На любом вертикальном пути будет не более
Доказательство разбивается на две части:
Следствие. На любом пути будет не более
Ради этого мы всё и делали: теперь построим какую-нибудь структуру на каждом тяжелого пути (например, дерево отрезков), а при ответе на запрос (скажем, суммы на пути) разобьем его на
Большинство публичных реализаций HLD — это 120-150 строк кода. Мы же воспользуемся следующим трюком, который сильно упростит нам жизнь: перенумеруем вершины дерева так, что для каждого тяжелого пути все его вершины будут иметь последовательные номера.
А именно, на этапе подсчёта размера поддеревьев, изменим список смежности каждой вершины так, чтобы в самом начале шел её «тяжелый» ребёнок. Тогда, если запустить обычный эйлеров обход графа, то tin
-ы и будут нужной нумерацией, потому что в каждой вершине мы шли в своего тяжелого ребёнка в первую очередь.
Теперь мы можем построить какую-нибудь структуру поверх массива размера
<int> g[maxn];
vectorint s[maxn], p[maxn], tin[maxn], tout[maxn];
int head[maxn]; // «голова» тяжелого пути, которому принадлежит v
int t = 0;
void sizes (int v = 0) {
[v] = 1;
sfor (int &u : g[v]) {
(u);
sizes[v] += s[u];
sif (s[u] > s[g[v][0]])
// &u -- это ссылка, так что её легально использовать при swap-е
(u, g[v][0]);
swap}
}
void hld (int v = 0) {
[v] = t++;
tinfor (int u : g[v]) {
// если это тяжелый ребенок -- его next нужно передать
// в противном случае он сам является головой нового пути
[u] = (u == g[v][0] ? head[v] : u);
head(u);
hld}
[v] = t;
tout}
Простейший пример задачи на HLD: дано дерево, каждой вершине которого приписано какое-то число, и поступают запросы двух типов:
Подвесим дерево за произвольную вершину и построим на нём HL-декомпозицию с деревом отрезков в качестве внутренней структуры. Его код мы приводить не будем и посчитаем, что оно реализовано примерно так же, как в соответствующей статье и имеет методы upd(k, x)
и get_min(l, r)
.
int val[maxn];
(0, n); segtree st
При операции обновления нам нужно просто обновить нужную ячейку в дереве отрезков:
void upd (int v, int x) {
.upd(tin[v], x);
st}
Запрос минимума сложнее: нам нужно разбить исходный запрос на запросы к вертикальным путям.
int ancestor (int a, int b) {
return tin[a] <= tin[b] && tin[b] < tout[a];
}
void up (int &a, int &b, int &ans) {
while (!ancestor(head[a], b)) {
= min(ans, st.get_min(tin[head[a]], tin[a]));
ans = p[head[a]];
a }
}
int get_min (int a, int b) {
int ans = inf;
(a, b, ans);
up(b, a, ans);
upif (!ancestor(a, b))
(a, b);
swap= min(ans, st.get_min(tin[a], tin[b]));
ans return ans;
}