Link-Cut Tree
Link-cut tree (dinamic-tree) — это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции:
- min(v) — искать минимум на пути от вершины до корня;
- add(v, c) — прибавлять константу на пути от вершины до корня;
- link(u, w) — подвешивать одно дерево на другое;
- cut(v) — отрезать дерево с корнем в вершине v.
Среднее время выполнения каждой операции -
. Эта структура данных была придумана Робертом Тарьяном и Даниелем Слейтером в 1982 году.Содержание
Решение задачи в частном случае
Сначала научимся выполнять эти операции для частного случая, в котором все деревья - это пути. Для этого представим путь в виде дерева, в котором ключи выбираются равными глубине вершины.Тогда операциям
и будут соответствовать и .Чтобы прибавлять заданное число на пути от вершины до корня, будем в каждой вершине хранить величину
, которая равна разнице между весом вершины и весом её родителя. Для корня это значение равно весу самого корня. Поэтому вес вершины определятся следующим образом:, где сумма берется по всем предкам , включая саму вершину.
При прибавлении
на пути от вершины до корня, сначала вызывается , после чего в левом поддереве находятся вершины, которые лежат на пути к корню. Затем надо прибавить к и, чтобы сохранить веса вершин, которые находятся ниже в пути, вычесть от .Для поиска минимума поступим аналогично. Определим
таким образом, чтобы сохранялся следующий инвариант: . Пусть и дети , тогда
Чтобы найти минимум на пути, надо вызвать
, а затем сравнить вес и минимум её левого ребенка.Link-cut tree
Чтобы обобщить, разобьем дерево на множество непересекающихся путей. Каждое ребро обозначим либо solid-ребром, либо dashed-ребром. Все пути в link-cut дереве хранятся в виде splay-деревьев. Корень каждого splay-дерева хранит указатель на вершину-родителя. В дальнейшем будем называть этот указатель
.expose(u)
Ключевая операция в link-cut-деревьях —
. После её выполнения лежит на одном пути с корнем link-cut дерева и при этом становится корнем в splay-дереве получившегося пути.expose(u) 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-дереве (то есть все вершины пути без ), а в правом - те, что ниже. Тогда прибавим к и вычтем константу от правого ребенка , чтобы скомпенсировать разницу и сохранить инвариант.add(v, c) expose(v) Δw(v) += c Δw(right(v)) -= c
min(v)
Построим splay-дерево для пути и сравним минимум корня
c минимумом в левом поддереве:min(v) expose(v) if (Δmin(left(v)) + Δw(left(v)) < Δw(v)) then return Δmin(left(v)) + Δw(left(v)) else return Δw(v)
link(v, u)
Если
- корень, а - вершина в другом дереве, то соединяет два дерева добавлением ребра , причем становится родителем .link(v, u) 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)))}
cut(v)
Отрезает дерево с корнем
. После вызова станет корнем splay-дерева, и в правом поддереве будут содержатся все вершины, которые были ниже в link-cut дереве, а в левом - те что выше. Обнулив указатель на левого ребенка и на родителя в левом поддереве, получим требуемое.cut(v) 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 - лес, получившийся из после одного вызова . Определим потенциал , - увеличение после одной операции .Лемма: |
Доказательство: |
Теперь проанализируем . Используя тот факт, что начальное значение не превосходит , приходим к тому, что для деревьев с вершинами, по крайней мере за операцию , среднее на одну операцию будет не больше, чем
Докажем, что амортизационная стоимость операции
равнаПусть лемме стоимость i-той операции не превосходит . Это приводит к тому, что амортизационная стоимость ограничена следующим значением:
— количество вершин в поддеревьях ( здесь имеется в виду splay-дерево пути, котоый строится в ходе выполнения ), . По
Здесь
, поэтому амортизационная стоимость равна