Heavy-light декомпозиция

Материал из Викиконспекты
Перейти к: навигация, поиск

Heavy-light декомпозиция — техника разбиения дерева на множество путей для решения задач о запросах на пути в дереве (в том числе с модификациями).

Описание задачи

Задача:
Пусть у нас есть дерево [math]T[/math] c [math]n[/math] вершинами и нам нужно проводить операции на нем на пути от вершины [math]v[/math] до вершины [math]u[/math]. (Например сумма на пути с модификацией прибавления на пути)

Множество подобных запросов делаются за время [math]O(\log^2{n})[/math] с помощью Heavy-light декомпозиции.

Описание декомпозиции

Пример разбиения на лёгкие/тяжёлые рёбра.

Декомпозиция заключается в классификации всех рёбер дерева [math]T[/math] в 2 вида: легкие и тяжёлые. Введём функцию [math]s(v)[/math], которая будет обозначать размер поддерева вершины [math]v[/math].

Тяжёлые ребра — ребра [math](u, v)[/math] такие, что [math]s(v) \geqslant[/math] [math]\frac{s(u)}{2}[/math].

Лёгкие ребра — соответственно все остальные.

Очевидно, что из вершины может выходить как максимум одно тяжёлое ребро, т.к. иначе у нас есть два поддерева по как минимум [math]\frac{s(u)}{2}[/math] вершин, а также сама вершина [math]u[/math]. Итого [math]s(u) + 1[/math] вершин, тогда как у нас всего [math]s(u)[/math] вершин в поддереве.

Теперь рассмотрим вершины, из которых не ведет ни одно тяжёлое ребро. Будем идти от них вверх до корня или пока не пройдем легкое ребро. Получится какое-то множество путей. Утверждается, что оно будет являться искомым и корректным.

Существует вариант Heavy-light декомпозиции на вершинно-непересекающихся путях. Чтобы получить такой путь нужно всего-лишь выкинуть последнее ребро из всех путей в рёберно-непересекающейся декомпозиции. Это может быть удобно при решении задач, где веса находятся не на рёбрах, а на вершинах и соответствующие запросы также делаются на вершинах.

Доказательство корректности полученной декомпозиции

Утверждение:
Полученная декомпозиция является искомой.
[math]\triangleright[/math]

Докажем по отдельности корректность декомпозиции.

  1. Все рёбра покрыты путями.
    Есть два типа вершин: те, из которых ведёт ровно одно тяжёлое ребро и те, из которых не ведёт ни одного тяжёлого ребра. Для первого типа вершин мы дойдем до них некоторым путём через тяжёлое ребро снизу по определению выбора путей, а лёгкие рёбра ведущие из неё возьмем как последние рёбра в соответствующих путях. Для второго типа вершин мы по определению выбора путей возьмем их как начальные и пойдем вверх.
    Таким образом все рёбра будут покрыты.
  2. Все пути не пересекаются.
    Докажем от противного. Пусть мы взяли какое-то ребро дважды. Это значит, что из какой-то вершины вело более чем одно тяжёлое ребро, чего быть не могло. Получили противоречие.
  3. При прохождении пути от вершины [math]v[/math] до вершины [math]u[/math] произойдет смена не более, чем [math]O(\log{n})[/math] путей.
    Докажем эквивалентный факт, что при пути от любой вершины до корня мы сменим не более, чем [math]O(\log{n})[/math] путей. Рассмотрим лёгкое ребро. Заметим, что проход вниз по такому ребру уменьшает размер поддерева как минимум в 2 раза. Но смена пути может произойти только при переходе по лёгкому ребру. Таким образом мы сменим не более [math]O(\log{n})[/math] путей.
[math]\triangleleft[/math]

Применение

Сумма на пути

Классическая задача о сумме на пути в дереве с [math]n[/math] решается с помощью Heavy-light декомпозиции за время. Возможны модификации веса.

Построим дерево отрезков над каждым путём. Рассмотрим запрос [math]sum(u, v)[/math]. Давайте найдем вершину [math]c[/math], которая является [math]LCA(u, v)[/math] (например с помощью двоичного подъема. Мы разбили запрос на два: [math](u, c)[/math] и [math](c, v)[/math], на каждый из которых можно легко ответить выбирая путь, содержащий самую нижнюю вершину и вырезать его, пока этот путь не содержит [math]c[/math].

Так мы сможем получать ответ на пути за [math]O(\log{n})[/math]. А всего таких путей нужно будет рассмотреть [math]O(\log{n})[/math]. Итого мы способны решить эту задачу за время [math]O(\log^2{n})[/math].

Хоть это и не самый эффективный способ для решения этой задачи, но можно заметить, что с помощью такого трюка мы можем решить широкий класс задач просто сведя её к задаче о запросах в дереве отрезков (такие как максимум на пути, покраска на пути, прибавление на пути и др.).

Реализация

Опущены некоторые детали реализации: построение и предподсчёт. [math]getPath[/math] — функция, позволяющая найти номер пути, относящийся к конкретной вершине, [math]pathRoot[/math] — корень пути (самая верхняя вершина), [math]pathPos[/math] — смещение вершины в пути относительно корня пути, [math]getValue(i, j)[/math] — вес [math]j[/math]-ого ребра в [math]i[/math]-ом пути.

Пример реализации запроса суммы на пути:

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

См.также

Источники информации