Heavy-light декомпозиция — различия между версиями
Строка 60: | Строка 60: | ||
Итого мы рассмотрели корень каждого пути за <tex>O(1)</tex>, а путей всего <tex>O(\log{n})</tex>. И в последнем пути один раз запустили бинпоиск, который отработал за <tex>O(\log{n})</tex>. Итоговая асимптотика <tex>O(\log{n})</tex>. | Итого мы рассмотрели корень каждого пути за <tex>O(1)</tex>, а путей всего <tex>O(\log{n})</tex>. И в последнем пути один раз запустили бинпоиск, который отработал за <tex>O(\log{n})</tex>. Итоговая асимптотика <tex>O(\log{n})</tex>. | ||
==Реализация== | ==Реализация== | ||
− | Опущены некоторые детали реализации: построение и | + | Опущены некоторые детали реализации: построение и дерево отрезков. |
<ul> | <ul> | ||
− | |||
− | |||
<li><tex>\mathrm{pathPos}</tex> {{---}} функция, позволяющая найти смещение вершины в пути относительно корня пути,</li> | <li><tex>\mathrm{pathPos}</tex> {{---}} функция, позволяющая найти смещение вершины в пути относительно корня пути,</li> | ||
<li><tex>\mathrm{getValue(\mathtt{i}, \mathtt{j})}</tex> {{---}} функция, позволяющая найти вес <tex>\mathtt{j}</tex>-ого ребра в <tex>\mathtt{i}</tex>-ом пути.</li> | <li><tex>\mathrm{getValue(\mathtt{i}, \mathtt{j})}</tex> {{---}} функция, позволяющая найти вес <tex>\mathtt{j}</tex>-ого ребра в <tex>\mathtt{i}</tex>-ом пути.</li> | ||
Строка 69: | Строка 67: | ||
Пример реализации запроса суммы на пути: | Пример реализации запроса суммы на пути: | ||
<code> | <code> | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
'''int''' query('''int''' u, '''int''' v): | '''int''' query('''int''' u, '''int''' v): | ||
'''int''' res = 0 | '''int''' res = 0 | ||
− | '''int''' root = | + | '''int''' root = корень пути, в котором находится u |
− | '''while''' | + | '''while''' root не является предком v: <font color=green>// поднимаемся до тех пор, пока наш путь не содержит общего предка u и v</font> |
− | res += | + | segmentTree = дерево отрезков, соответствующее путю в котором лежит u |
− | u = | + | res += segmentTree.sum(0, pathPos(u)) |
− | root = | + | u = предок root <font color=green>// вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути</font> |
− | root = | + | root = корень пути, в котором находится u |
− | '''while''' | + | root = корень пути, в котором находится v |
− | res += | + | '''while''' root не является предком u: <font color=green>// аналогично прошлому while, но с другой стороны</font> |
− | v = | + | segmentTree = дерево отрезков, соответствующее путю в котором лежит v |
− | root = | + | res += segmentTree.sum(0, pathPos(v)) |
+ | v = предок root | ||
+ | root = корень пути, в котором находится v | ||
<font color=green>// последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами</font> | <font color=green>// последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами</font> | ||
− | res += | + | segmentTree = дерево отрезков, соответствующее путю в котором лежит u |
+ | res += segmentTree.sum(min(pathPos(u), pathPos(v)), max(pathPos(u), pathPos(v))) | ||
'''return''' res | '''return''' res | ||
</code> | </code> |
Версия 21:52, 8 июня 2015
Heavy-light декомпозиция — техника разбиения подвешенного дерева на множество путей для решения задач о запросах на пути в дереве (в том числе с модификациями).
Содержание
Описание задачи
Задача: |
Имеется подвешенное дерево | c вершинами и необходимо проводить операции на нем на пути от вершины до вершины .
Примеры запросов:
- сумма на пути,
- максимум на пути,
- количество рёбер на пути, вес которых больше заданного .
Примеры модификаций:
- модификация одного ребра,
- прибавление к весу всех рёбер на пути,
- установка веса всех рёбер на пути в заданное .
Множество подобных запросов делаются за время за полином от логарифма (обычно
) с помощью Heavy-light декомпозиции.Описание декомпозиции
Декомпозиция заключается в классификации всех рёбер дерева
в 2 вида: легкие и тяжёлые. Введём функцию , которая будет обозначать размер поддерева вершины .Тяжёлые ребра (англ. heavy edge) — ребра
такие, что .Лёгкие ребра (англ. light edge) — соответственно все остальные.
Очевидно, что из вершины может выходить как максимум одно тяжёлое ребро, т.к. иначе у нас есть два поддерева по как минимум
вершин, а также сама вершина . Итого вершин, тогда как у нас всего вершин в поддереве.Теперь рассмотрим вершины, из которых не ведет вниз ни одно тяжёлое ребро. Будем идти от них вверх до корня или пока не пройдем легкое ребро. Получится какое-то множество путей. Утверждается, что полученная таким путём декомпозиция будет являться искомой и корректной.
Утверждение: |
Полученная декомпозиция является искомой. |
Докажем по отдельности корректность декомпозиции.
|
Существует вариант Heavy-light декомпозиции на вершинно-непересекающихся путях. Чтобы получить такой путь нужно всего-лишь выкинуть последнее ребро из всех путей в рёберно-непересекающейся декомпозиции. Это может быть удобно при решении задач, где веса находятся не на рёбрах, а на вершинах и соответствующие запросы также делаются на вершинах.
Применение
Сумма на пути
Классическая задача о сумме на пути в дереве с
решается с помощью Heavy-light декомпозиции за время . Возможны модификации веса.Построим дерево отрезков над каждым путём. Рассмотрим запрос . Давайте найдем вершину , которая является (например с помощью двоичного подъема. Мы разбили запрос на два: и , на каждый из которых можно легко ответить выбирая путь, содержащий самую нижнюю вершину и вырезать его, пока этот путь не содержит .
Так мы сможем получать ответ на пути за
. А всего таких путей нужно будет рассмотреть . Итого мы способны решить эту задачу за время .Хоть это и не самый эффективный способ для решения этой задачи, но можно заметить, что с помощью такого трюка мы можем решить широкий класс задач просто сведя её к задаче о запросах в дереве отрезков (такие как максимум на пути, покраска на пути, прибавление на пути и др.).
LCA
Задача о наименьшем общем предке для двух вершин в дереве с
решается с помощью Heavy-light декомпозиции за время .Мы можем проверять, что вершина является предком другой вершины за
с помощью времен входа\выхода в каждую вершину. Тогда рассмотрим самый нижний путь (содержащий первую вершину) и посмотрим на его корень (самая высокая вершина в пути). Если эта вершина является предком второй вершины, то ответ где-то в этом пути, иначе выкинем этот путь и рассмотрим следующий вверх за ним.Когда мы определили путь, в котором содержится ответ, мы можем с помощью бинпоиска найти в нём первую вершину, являющуюся предком второй вершины. Эта вершина является ответом.
Итого мы рассмотрели корень каждого пути за
, а путей всего . И в последнем пути один раз запустили бинпоиск, который отработал за . Итоговая асимптотика .Реализация
Опущены некоторые детали реализации: построение и дерево отрезков.
- — функция, позволяющая найти смещение вершины в пути относительно корня пути,
- — функция, позволяющая найти вес -ого ребра в -ом пути.
Пример реализации запроса суммы на пути:
int query(int u, int v): int res = 0 int root = корень пути, в котором находится u while root не является предком v: // поднимаемся до тех пор, пока наш путь не содержит общего предка u и v segmentTree = дерево отрезков, соответствующее путю в котором лежит u res += segmentTree.sum(0, pathPos(u)) u = предок root // вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути root = корень пути, в котором находится u root = корень пути, в котором находится v while root не является предком u: // аналогично прошлому while, но с другой стороны segmentTree = дерево отрезков, соответствующее путю в котором лежит v res += segmentTree.sum(0, pathPos(v)) v = предок root root = корень пути, в котором находится v // последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами segmentTree = дерево отрезков, соответствующее путю в котором лежит u res += segmentTree.sum(min(pathPos(u), pathPos(v)), max(pathPos(u), pathPos(v))) return res