Heavy-light декомпозиция — это мощный метод решения задач на запросы на путях, когда также есть запросы обновлений. Если запросов обновления нет, то лучше написать что-нибудь попроще.
Heavy-light декомпозицией корневого дерева называется результат следующего процесса: каждой вершины посмотрим на всех её непосредственных детей , выберем среди них ребёнка с самым большим размером поддерева — если таких больше одного, то любого из них — и назовём ребро тяжелым (англ. heavy), а все остальные рёбра — лёгкими (англ. light).
Таким образом, дерево распалось на тяжелые и лёгкие рёбра. Это разбиение и называют heavy-light декомпозицией.

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