Heavy-light декомпозиция — различия между версиями

Материал из Викиконспекты
Перейти к: навигация, поиск
Строка 1: Строка 1:
'''Heavy-light декомпозиция''' {{---}} техника разбиения дерева на множество путей для решения задач о запросах на пути в дереве (в том числе с модификациями).
+
'''Heavy-light декомпозиция''' {{---}} техника разбиения подвешенного дерева на множество путей для решения задач о запросах на пути в дереве (в том числе с модификациями).
 
==Описание задачи==
 
==Описание задачи==
 
{{Задача
 
{{Задача
|definition=Пусть у нас есть дерево <tex>T</tex> c <tex>n</tex> вершинами и нам нужно проводить операции на нем на пути от вершины <tex>v</tex> до вершины <tex>u</tex>. (Например сумма на пути с модификацией прибавления на пути)}}
+
|definition=Имеется дерево <tex>T</tex> c <tex>n</tex> вершинами и необходимо проводить операции на нем на пути от вершины <tex>v</tex> до вершины <tex>u</tex>.}}
Множество подобных запросов делаются за время <tex>O(\log^2{n})</tex> с помощью Heavy-light декомпозиции.
+
Примеры запросов:
 +
<ul>
 +
<li>Сумма на пути</li>
 +
<li>Максимум на пути</li>
 +
<li>Количество рёбер на пути, вес которых больше заданного <tex>c</tex></li>
 +
</ul>
 +
 
 +
Примеры модификаций:
 +
<ul>
 +
<li>Модификация одного ребра</li>
 +
<li>Прибавление к весу всех рёбер на пути</li>
 +
<li>Установка веса всех рёбер на пути в заданное <tex>c</tex></li>
 +
</ul>
 +
 
 +
Множество подобных запросов делаются за время за полином от логарифма (обычно <tex>O(\log^2{n})</tex>) с помощью Heavy-light декомпозиции.
 
==Описание декомпозиции==
 
==Описание декомпозиции==
 
[[Файл:Heavylight.png|thumb|right|800x280px|Пример разбиения. В вершинах указан размер поддерева.]]
 
[[Файл:Heavylight.png|thumb|right|800x280px|Пример разбиения. В вершинах указан размер поддерева.]]
 
Декомпозиция заключается в классификации всех рёбер дерева <tex>T</tex> в 2 вида: легкие и тяжёлые. Введём функцию <tex>s(v)</tex>, которая будет обозначать размер поддерева вершины <tex>v</tex>.
 
Декомпозиция заключается в классификации всех рёбер дерева <tex>T</tex> в 2 вида: легкие и тяжёлые. Введём функцию <tex>s(v)</tex>, которая будет обозначать размер поддерева вершины <tex>v</tex>.
  
'''Тяжёлые ребра''' {{---}} ребра <tex>(u, v)</tex> такие, что <tex>s(v) \geqslant</tex> <tex dpi="150">\frac{s(u)}{2}</tex>.
+
'''Тяжёлые ребра (Heavy edge)''' {{---}} ребра <tex>(u, v)</tex> такие, что <tex>s(v) \geqslant</tex> <tex dpi="150">\frac{s(u)}{2}</tex>.
  
'''Лёгкие ребра''' {{---}} соответственно все остальные.
+
'''Лёгкие ребра (Light edge)''' {{---}} соответственно все остальные.
  
 
Очевидно, что из вершины может выходить как максимум одно тяжёлое ребро, т.к. иначе у нас есть два поддерева по как минимум <tex dpi="150">\frac{s(u)}{2}</tex> вершин, а также сама вершина <tex>u</tex>. Итого <tex>s(u) + 1</tex> вершин, тогда как у нас всего <tex>s(u)</tex> вершин в поддереве.
 
Очевидно, что из вершины может выходить как максимум одно тяжёлое ребро, т.к. иначе у нас есть два поддерева по как минимум <tex dpi="150">\frac{s(u)}{2}</tex> вершин, а также сама вершина <tex>u</tex>. Итого <tex>s(u) + 1</tex> вершин, тогда как у нас всего <tex>s(u)</tex> вершин в поддереве.
  
Теперь рассмотрим вершины, из которых не ведет ни одно тяжёлое ребро. Будем идти от них вверх до корня или пока не пройдем легкое ребро. Получится какое-то множество путей. Утверждается, что полученная таким путём декомпозиция будет являться искомой и корректной.
+
Теперь рассмотрим вершины, из которых не ведет вниз ни одно тяжёлое ребро. Будем идти от них вверх до корня или пока не пройдем легкое ребро. Получится какое-то множество путей. Утверждается, что полученная таким путём декомпозиция будет являться искомой и корректной.
  
Существует вариант Heavy-light декомпозиции на вершинно-непересекающихся путях. Чтобы получить такой путь нужно всего-лишь выкинуть последнее ребро из всех путей в рёберно-непересекающейся декомпозиции. Это может быть удобно при решении задач, где веса находятся не на рёбрах, а на вершинах и соответствующие запросы также делаются на вершинах.
 
==Доказательство корректности полученной декомпозиции==
 
 
{{Утверждение
 
{{Утверждение
 
|statement = Полученная декомпозиция является искомой.
 
|statement = Полученная декомпозиция является искомой.
Строка 26: Строка 38:
 
# При прохождении пути от вершины <tex>v</tex> до вершины <tex>u</tex> произойдет смена не более, чем <tex>O(\log{n})</tex> путей. <br>Докажем эквивалентный факт, что при пути от любой вершины до корня мы сменим не более, чем <tex>O(\log{n})</tex> путей. Рассмотрим лёгкое ребро. Заметим, что проход вниз по такому ребру уменьшает размер поддерева как минимум в 2 раза. Но смена пути может произойти только при переходе по лёгкому ребру. Таким образом мы сменим не более <tex>O(\log{n})</tex> путей.
 
# При прохождении пути от вершины <tex>v</tex> до вершины <tex>u</tex> произойдет смена не более, чем <tex>O(\log{n})</tex> путей. <br>Докажем эквивалентный факт, что при пути от любой вершины до корня мы сменим не более, чем <tex>O(\log{n})</tex> путей. Рассмотрим лёгкое ребро. Заметим, что проход вниз по такому ребру уменьшает размер поддерева как минимум в 2 раза. Но смена пути может произойти только при переходе по лёгкому ребру. Таким образом мы сменим не более <tex>O(\log{n})</tex> путей.
 
}}
 
}}
 +
 +
 +
Существует вариант Heavy-light декомпозиции на вершинно-непересекающихся путях. Чтобы получить такой путь нужно всего-лишь выкинуть последнее ребро из всех путей в рёберно-непересекающейся декомпозиции. Это может быть удобно при решении задач, где веса находятся не на рёбрах, а на вершинах и соответствующие запросы также делаются на вершинах.
  
 
==Применение==
 
==Применение==
Строка 37: Строка 52:
 
Хоть это и не самый эффективный способ для решения этой задачи, но можно заметить, что с помощью такого трюка мы можем решить широкий класс задач просто сведя её к задаче о запросах в дереве отрезков (такие как максимум на пути, покраска на пути, прибавление на пути и др.).
 
Хоть это и не самый эффективный способ для решения этой задачи, но можно заметить, что с помощью такого трюка мы можем решить широкий класс задач просто сведя её к задаче о запросах в дереве отрезков (такие как максимум на пути, покраска на пути, прибавление на пути и др.).
 
==Реализация==
 
==Реализация==
Опущены некоторые детали реализации: построение и предподсчёт. <tex>getPath</tex> {{---}} функция, позволяющая найти номер пути, относящийся к конкретной вершине, <tex>pathRoot</tex> {{---}} корень пути (самая верхняя вершина), <tex>pathPos</tex> {{---}} смещение вершины в пути относительно корня пути, <tex>getValue(i, j)</tex> {{---}} вес <tex>j</tex>-ого ребра в <tex>i</tex>-ом пути.
+
Опущены некоторые детали реализации: построение и предподсчёт.
 
+
<ul>
 +
<li><tex>\mathrm{getPath}</tex> {{---}} функция, позволяющая найти номер пути, относящийся к конкретной вершине.</li>
 +
<li><tex>\mathrm{pathRoot}</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>
 +
</ul>
 
Пример реализации запроса суммы на пути:
 
Пример реализации запроса суммы на пути:
 
<code>
 
<code>
  
  '''function''' queryPath('''int''' path, '''int''' from, '''int''' to):
+
  '''int''' queryPath('''int''' path, '''int''' from, '''int''' to):
 
     '''int''' res = 0
 
     '''int''' res = 0
 
     '''while''' from <= to:
 
     '''while''' from <= to:
         '''if''' from % 2 == 1:
+
         '''if''' from '''mod''' 2 == 1:
 
             res += getValue(path, from)
 
             res += getValue(path, from)
         '''if''' to % 2 == 0:
+
         '''if''' to '''mod''' 2 == 0:
 
             res += getValue(path, to)
 
             res += getValue(path, to)
 
         to = (to - 1) / 2
 
         to = (to - 1) / 2
Строка 53: Строка 73:
 
     '''return''' res
 
     '''return''' res
  
  '''function''' query('''int''' u, '''int''' v):
+
  '''int''' query('''int''' u, '''int''' v):
 
     '''int''' res = 0
 
     '''int''' res = 0
 
     '''int''' root = pathRoot(u)
 
     '''int''' root = pathRoot(u)
     '''while''' !isAncestor(root, v) <font color=green>// поднимаемся до тех пор, пока наш путь не содержит общего предка u и v</font>
+
     '''while''' '''not''' (root '''is ancestor of''' v) <font color=green>// поднимаемся до тех пор, пока наш путь не содержит общего предка u и v</font>
 
         res += queryPath(getPath(u), 0, pathPos(u))
 
         res += queryPath(getPath(u), 0, pathPos(u))
         u = getParent(root<font color=green>// вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути</font>
+
         u = '''parent of''' root           <font color=green>// вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути</font>
 
         root = pathRoot(u)
 
         root = pathRoot(u)
 
     root = pathRoot(v)
 
     root = pathRoot(v)
     '''while''' !isAncestor(root, u) <font color=green>// аналогично прошлому while, но с другой стороны</font>
+
     '''while''' '''not''' (root '''is ancestor of''' u) <font color=green>// аналогично прошлому while, но с другой стороны</font>
 
         res += queryPath(getPath(v), 0, pathPos(v))
 
         res += queryPath(getPath(v), 0, pathPos(v))
         v = getParent(root)
+
         v = '''parent of''' root
 
         root = pathRoot(v)
 
         root = pathRoot(v)
 
     <font color=green>// последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами</font>
 
     <font color=green>// последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами</font>

Версия 13:38, 8 июня 2015

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

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

Задача:
Имеется дерево [math]T[/math] c [math]n[/math] вершинами и необходимо проводить операции на нем на пути от вершины [math]v[/math] до вершины [math]u[/math].

Примеры запросов:

  • Сумма на пути
  • Максимум на пути
  • Количество рёбер на пути, вес которых больше заданного [math]c[/math]

Примеры модификаций:

  • Модификация одного ребра
  • Прибавление к весу всех рёбер на пути
  • Установка веса всех рёбер на пути в заданное [math]c[/math]

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

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

Пример разбиения. В вершинах указан размер поддерева.

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

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

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

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

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

Утверждение:
Полученная декомпозиция является искомой.
[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]


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

Применение

Сумма на пути

Классическая задача о сумме на пути в дереве с [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]\mathrm{getPath}[/math] — функция, позволяющая найти номер пути, относящийся к конкретной вершине.
  • [math]\mathrm{pathRoot}[/math] — функция, позволяющая найти корень пути (самую верхнюю вершину).
  • [math]\mathrm{pathPos}[/math] — функция, позволяющая найти смещение вершины в пути относительно корня пути.
  • [math]\mathrm{getValue(\mathtt{i}, \mathtt{j})}[/math] — функция, позволяющая найти вес [math]\mathtt{j}[/math]-ого ребра в [math]\mathtt{i}[/math]-ом пути.

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

int queryPath(int path, int from, int to):
    int res = 0
    while from <= to:
        if from mod 2 == 1:
            res += getValue(path, from)
        if to mod 2 == 0:
            res += getValue(path, to)
        to = (to - 1) / 2
        from = (from + 1) / 2
    return res
int query(int u, int v):
    int res = 0
    int root = pathRoot(u)
    while not (root is ancestor of v) // поднимаемся до тех пор, пока наш путь не содержит общего предка u и v
        res += queryPath(getPath(u), 0, pathPos(u))
        u = parent of root            // вырезали нижний путь и подняли нижнюю вершину до нижней вершины следующего пути
        root = pathRoot(u)
    root = pathRoot(v)
    while not (root is ancestor of u) // аналогично прошлому while, но с другой стороны
        res += queryPath(getPath(v), 0, pathPos(v))
        v = parent of root
        root = pathRoot(v)
    // последний путь (тот, что содержит общего предка) обрезан с двух сторон полученными вершинами
    res += queryPath(path(u), min(pathPos(u), pathPos(v)), max(pathPos(u), pathPos(v)))
    return res

См.также

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