Link-Cut Tree — различия между версиями
(→Источники информации) |
|||
| Строка 1: | Строка 1: | ||
| − | Динамические деревья (англ.''link-cut trees'') используются в двух областях: потоки и динамические графы. | + | Динамические деревья (англ. ''link-cut trees'') используются в двух областях: потоки и динамические графы. |
В первом случае динамические деревья позволяют построить эффективные алгоритмы для задачи о поиске максимального потока. | В первом случае динамические деревья позволяют построить эффективные алгоритмы для задачи о поиске максимального потока. | ||
В динамическом графе периодически происходят изменения: появляются и исчезают ребра, меняются их веса. Изменения нужно быстро обрабатывать, а также уметь проверять связность, искать диаметр. Динамические деревья являются инструментом, который позволяет легко решать эти задачи. Динамические деревья особенно эффективны, когда нужно работать с большими деревьями и большим количеством запросов '''link''' и '''cut'''. | В динамическом графе периодически происходят изменения: появляются и исчезают ребра, меняются их веса. Изменения нужно быстро обрабатывать, а также уметь проверять связность, искать диаметр. Динамические деревья являются инструментом, который позволяет легко решать эти задачи. Динамические деревья особенно эффективны, когда нужно работать с большими деревьями и большим количеством запросов '''link''' и '''cut'''. | ||
| − | '''Link-cut tree''' (''dynamic tree'') {{---}} это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции: | + | '''Link-cut tree''' (англ. ''dynamic tree'') {{---}} это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции: |
* '''min(v)''' {{---}} искать минимум на пути от вершины до корня; | * '''min(v)''' {{---}} искать минимум на пути от вершины до корня; | ||
* '''add(v, c)''' {{---}} прибавлять константу на пути от вершины до корня; | * '''add(v, c)''' {{---}} прибавлять константу на пути от вершины до корня; | ||
Версия 22:53, 2 марта 2016
Динамические деревья (англ. link-cut trees) используются в двух областях: потоки и динамические графы. В первом случае динамические деревья позволяют построить эффективные алгоритмы для задачи о поиске максимального потока. В динамическом графе периодически происходят изменения: появляются и исчезают ребра, меняются их веса. Изменения нужно быстро обрабатывать, а также уметь проверять связность, искать диаметр. Динамические деревья являются инструментом, который позволяет легко решать эти задачи. Динамические деревья особенно эффективны, когда нужно работать с большими деревьями и большим количеством запросов link и cut.
Link-cut tree (англ. dynamic tree) — это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции:
- min(v) — искать минимум на пути от вершины до корня;
- add(v, c) — прибавлять константу на пути от вершины до корня;
- link(u, w) — подвешивать одно дерево на другое;
- cut(v) — отрезать дерево с корнем в вершине v.
Среднее время выполнения каждой операции — . Эта структура данных была придумана Робертом Тарьяном и Даниелем Слейтером в 1982 году.
Содержание
Решение задачи в частном случае
Сначала рассмотрим частный случай, в котором все деревья — это пути, и мы хотим уметь:
- прибавлять константу и искать минимум на некотором суффиксе (то есть на пути от вершины до корня),
- разбить один путь на два,
- подвешивать голову одного пути к хвосту другого.
Если бы не последние две операции, то можно было бы применить дерево отрезков, сложив в него вершины в том порядке в котором они идут в пути. Но непонятно, как сливать или разрезать деревья отрезков. Если использовать какие-нибудь сливаемые деревья, то и реализуются просто. Осталось научиться искать минимум и прибавлять константу на пути. Для этого, как и в деревьях отрезков, будем хранить дополнительные значения в вершинах. В качестве сливаемых деревьев выберем splay-деревья, в которых ключи выбираются равными глубине вершины.
Тогда операции будет соответствовать .
соединяет голову первого пути с хвостом второго. Используем функцию , которая вызовет от хвоста второго пути и сделает первый путь правым ребенком корня , то есть теперь находится ниже, чем .
Чтобы прибавлять заданное число на пути от вершины до корня, будем в каждой вершине хранить величину , которая равна разнице между весом вершины и весом её родителя. Для корня это значение равно весу самого корня. Поэтому вес вершины определятся следующим образом:
, где — все предки вершины .
При прибавлении на пути от вершины до корня, сначала вызывается , после чего в левом поддереве находятся вершины, которые лежат на пути к корню. Затем надо прибавить к и, чтобы сохранить веса вершин, которые находятся ниже в пути, вычесть от .
Для реализации будем хранить минимум уже для всего поддерева. Чтобы искать минимум от вершины , надо вызвать и сравнить её вес с минимумом левого поддерева, в котором теперь находятся все вершины пути кроме . Определим таким образом, чтобы сохранялся следующий инвариант: . Пусть и дети , тогда
Link-cut tree
Чтобы обобщить, разобьем дерево на множество непересекающихся путей. Каждое ребро обозначим либо solid-ребром, либо dashed-ребром. Все пути в link-cut дереве хранятся в виде splay-деревьев. Корень каждого splay-дерева хранит указатель на вершину-родителя. В дальнейшем будем называть этот указатель .
expose(u)
Ключевая операция в link-cut-деревьях — . После её выполнения лежит на одном пути с корнем link-cut дерева и при этом становится корнем в splay-дереве получившегося пути. Для этого она поднимается вверх по link-cut дереву, и если какой-нибудь путь пересекает путь от до корня, то она его отрезает, разъединяя splay-дерево и делая соответствующее solid-ребро dashed-ребром.
function expose(u: tree):
splay(u)
v u
while v root
p pathparent(v) //получаем указатель на ближайшую вершину пути, пересекающего путь от u до корня
splay(p) //теперь в правом поддереве p находятся вершины пути, которые находятся ниже чем p в link-cut-дереве,
parent(right(p)) null //поэтому правое поддерево p делаем новым путем
pathparent(right(p)) p
right(p) v //объединяем оставшийся и построенный пути
w(v) -= w(p)
min(p) min{0, min(left(p)) + w(left(p)), min(right(p)) + w(right(p))}
pathparent(v) null
v p
splay(u)
add(v, c)
Чтобы прибавить константу на пути от до корня link-cut-дерева вызовем , что построит запрашиваемый путь в виде splay-дерева, в котором — корень, и в левом поддереве находятся вершины, которые находятся выше чем в link-cut-дереве (то есть все вершины пути без ), а в правом — те, что ниже. Тогда прибавим к и вычтем константу от правого ребенка , чтобы скомпенсировать разницу и сохранить инвариант.
function add(v: tree, c: int):
expose(v)
w(v) += c
w(right(v)) -= c
min(v)
Построим splay-дерево для пути и сравним минимум корня c минимумом в левом поддереве:
int min(v : tree):
expose(v)
if min(left(v)) + w(left(v)) < w(v)
return min(left(v)) + w(left(v))
else
return w(v)
link(v, u)
Если — корень, а — вершина в другом дереве, то соединяет два дерева добавлением ребра , причем становится родителем .
tree link(v: tree, u: tree):
expose(v) //теперь v — корень в splay-дереве пути и не имеет левого ребенка(так как ключ равен глубине в link-cut дереве)
expose(u)
w(u) -= w(v) //чтобы сделать u родителем v в link-cut дереве 1. делаем путь, содержащий u, левым ребенком v в splay-дереве
parent(u) v // 2. обновляем w, min
left(v) u
min(v) min{0, min(u) + w(u), min(right(v)) + w((right(v)))}
return v
cut(v)
Отрезает дерево с корнем . После вызова вершина станет корнем splay-дерева, и в правом поддереве будут содержатся все вершины, которые были ниже в link-cut дереве, а в левом — те что выше. Обнулив указатель на левого ребенка и на родителя в левом поддереве, получим требуемое.
function cut(v: tree):
expose(v)
w(left(v)) += w(v)
min(v) min{0, min(right(v)) + w(right(v))}
left(v) null
parent(left(v)) null
Оценка времени работы
Назовем ребро из в её родителя тяжелым, если количество детей равное .
| Лемма: |
На пути от вершины до корня не больше легких ребер. |
| Доказательство: |
| Пусть — количество вершин в дереве с корнем в вершине, в которой мы сейчас находимся. Поднимаясь по легкому ребру, увеличивается в два раза, поэтому, пройдя больше легких ребер, получим . Значит, в дереве не больше легких ребер. |
Операция осуществляется с помощью последовательности преобразований dashed-ребра в solid-ребро и другого solid-ребра в dashed-ребро. Обозначим количество таких преобразований за . Найдем количество преобразований сделанных в течение . Пусть — множество всех тяжелых ребер, — все легкие ребра, — множество solid-ребер, преобразованных в dashed в течение одного , — множество dashed-ребер, преобразованных в solid.
По лемме, количество легких dashed-ребер, преобразованных в solid, будет не больше, чем .
Обозначим за лес деревьев, в которых каждое ребро либо solid, либо dashed, a — лес, получившийся из после одного вызова . Определим потенциал , — увеличение после одной операции .
| Лемма: |
| Доказательство: |
Теперь проанализируем . Используя тот факт, что начальное значение не превосходит , приходим к тому, что для деревьев с вершинами, по крайней мере за операцию , среднее на одну операцию будет не больше, чем
Докажем, что амортизационная стоимость операции равна
Пусть — количество вершин в поддеревьях ( здесь имеется в виду splay-дерево пути, котоый строится в ходе выполнения ), . По лемме стоимость i-той операции не превосходит . Это приводит к тому, что амортизационная стоимость ограничена следующим значением:
Здесь , поэтому амортизационная стоимость равна
Применение
LCA
C помощью link-cut-дерева можно найти наименьшего общего предка:
tree lca(u: tree, v: tree):
expose(u)
expose(v)
return pathparent(splay(u))
Первый вызов построит путь от до корня. Второй пересечет этот путь в наименьшем общем предке, поэтому в splay-дереве, которому принадлежит , хранится указатель на , после он будет находиться в .