Heavy-light декомпозиция
Heavy-light декомпозиция — техника разбиения дерева на множество путей для решения задач о запросах на пути в дереве (в том числе с модификациями).
Описание задачи
Задача: |
Пусть у нас есть дерево | c вершинами и нам нужно проводить операции на нем на пути от вершины до вершины . (Например сумма на пути с модификацией прибавления на пути)
Множество подобных запросов делаются за время
с помощью Heavy-light декомпозиции.Описание декомпозиции
Декомпозиция заключается в классификации всех рёбер дерева
в 2 вида: легкие и тяжёлые. Введём функцию , которая будет обозначать размер поддерева вершины .Тяжёлые ребра — ребра
такие, что .Лёгкие ребра — соответственно все остальные.
Очевидно, что из вершины может выходить как максимум одно тяжёлое ребро, т.к. иначе у нас есть два поддерева по как минимум
вершин, а также сама вершина . Итого вершин, тогда как у нас всего вершин в поддереве.Теперь рассмотрим вершины, из которых не ведет ни одно тяжёлое ребро. Будем идти от них вверх до корня или пока не пройдем легкое ребро. Получится какое-то множество путей. Утверждается, что оно будет являться искомым и корректным.
Существует вариант Heavy-light декомпозиции на вершинно-непересекающихся путях. Чтобы получить такой путь нужно всего-лишь выкинуть последнее ребро из всех путей в рёберно-непересекающейся декомпозиции. Это может быть удобно при решении задач, где веса находятся не на рёбрах, а на вершинах и соответствующие запросы также делаются на вершинах.
Доказательство корректности полученной декомпозиции
Утверждение: |
Полученная декомпозиция является искомой. |
Докажем по отдельности корректность декомпозиции.
|
Применение
Сумма на пути
Классическая задача о сумме на пути в дереве с
решается с помощью Heavy-light декомпозиции за время. Возможны модификации веса.Построим дерево отрезков над каждым путём. Рассмотрим запрос . Давайте найдем вершину , которая является (например с помощью двоичного подъема. Мы разбили запрос на два: и , на каждый из которых можно легко ответить выбирая путь, содержащий самую нижнюю вершину и вырезать его, пока этот путь не содержит .
Так мы сможем получать ответ на пути за
. А всего таких путей нужно будет рассмотреть . Итого мы способны решить эту задачу за время .Хоть это и не самый эффективный способ для решения этой задачи, но можно заметить, что с помощью такого трюка мы можем решить широкий класс задач просто сведя её к задаче о запросах в дереве отрезков (такие как максимум на пути, покраска на пути, прибавление на пути и др.).
Реализация
Опущены некоторые детали реализации: построение и предподсчёт.
— функция, позволяющая найти номер пути, относящийся к конкретной вершине, — корень пути (самая верхняя вершина), — смещение вершины в пути относительно корня пути, — вес -ого ребра в -ом пути.Пример реализации запроса суммы на пути:
function queryPath(int path, int from, int to): int res = 0 while from <= to: if from % 2 == 1: res += getValue(path, from) if to % 2 == 0: res += getValue(path, to) to = (to - 1) / 2 from = (from + 1) / 2 return res
function query(int u, int v): int res = 0 int root = pathRoot(u) while !isAncestor(root, v) // поднимаемся до тех пор, пока наш путь не содержит общего предка u и v res += queryPath(getPath(u), 0, pathPos(u)) u = getParent(root) // вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути root = pathRoot(u) root = pathRoot(v) while !isAncestor(root, u) // аналогично прошлому while, но с другой стороны res += queryPath(getPath(v), 0, pathPos(v)) v = getParent(root) root = pathRoot(v) // последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами res += queryPath(path(u), min(pathPos(u), pathPos(v)), max(pathPos(u), pathPos(v))) return res