Link-Cut Tree — различия между версиями
м (rollbackEdits.php mass rollback) |
|||
(не показано 6 промежуточных версий 4 участников) | |||
Строка 84: | Строка 84: | ||
===link(v, u)=== | ===link(v, u)=== | ||
− | Если <tex>v</tex> {{---}} корень, а <tex>u</tex> {{---}} вершина в другом дереве, то <tex>\mathrm{link(v, u)}</tex> соединяет два дерева добавлением ребра <tex>(v, u)</tex>, причем <tex> | + | Если <tex>v</tex> {{---}} корень, а <tex>u</tex> {{---}} вершина в другом дереве, то <tex>\mathrm{link(v, u)}</tex> соединяет два дерева добавлением ребра <tex>(v, u)</tex>, причем <tex>v</tex> становится родителем <tex>u</tex>. |
'''function''' link(v: '''tree''', u: '''tree'''): '''tree''' | '''function''' link(v: '''tree''', u: '''tree'''): '''tree''' | ||
Строка 96: | Строка 96: | ||
===cut(v)=== | ===cut(v)=== | ||
− | Отрезает дерево с корнем <tex>v</tex>. После вызова <tex>\mathrm{expose(v)}</tex> вершина <tex>v</tex> станет корнем splay-дерева, и в правом поддереве будут | + | Отрезает дерево с корнем <tex>v</tex>. После вызова <tex>\mathrm{expose(v)}</tex> вершина <tex>v</tex> станет корнем splay-дерева, и в правом поддереве будут содержаться все вершины, которые были ниже <tex>v</tex> в link-cut дереве, а в левом {{---}} те что выше. Обнулив указатель на левого ребенка <tex>v</tex> и на родителя в левом поддереве, получим требуемое. |
'''function''' cut(v: '''tree'''): | '''function''' cut(v: '''tree'''): | ||
Строка 102: | Строка 102: | ||
<tex>\vartriangle</tex>w(left(v)) += <tex>\vartriangle</tex>w(v) | <tex>\vartriangle</tex>w(left(v)) += <tex>\vartriangle</tex>w(v) | ||
<tex>\vartriangle</tex>min(v) <tex>=</tex> min{0, <tex>\vartriangle</tex>min(right(v)) + <tex>\vartriangle</tex>w(right(v))} | <tex>\vartriangle</tex>min(v) <tex>=</tex> min{0, <tex>\vartriangle</tex>min(right(v)) + <tex>\vartriangle</tex>w(right(v))} | ||
+ | parent(left(v)) <tex>=</tex> null | ||
left(v) <tex>=</tex> null | left(v) <tex>=</tex> null | ||
− | |||
==Оценка времени работы== | ==Оценка времени работы== | ||
Строка 114: | Строка 114: | ||
}} | }} | ||
− | Операция <tex>u</tex> осуществляется с помощью последовательности преобразований пунктирного ребра в сплошное ребро и другого сплошного ребра в пунктирное ребро. Обозначим количество таких преобразований за <tex>M</tex>. Найдем количество преобразований сделанных в течение <tex>\mathrm{expose(u)}</tex>. Пусть <tex>H</tex> {{---}} множество всех тяжелых ребер, <tex>L</tex> {{---}} все легкие ребра, <tex>S \rightarrow D</tex> {{---}} множество сплошных ребер, преобразованных в пунктирные в течение одного <tex>\mathrm{expose}</tex>, <tex>D \rightarrow S</tex> {{---}} множество пунктирных ребер, преобразованных в сплошные. | + | Операция <tex>\mathrm{expose(u)}</tex> осуществляется с помощью последовательности преобразований пунктирного ребра в сплошное ребро и другого сплошного ребра в пунктирное ребро. Обозначим количество таких преобразований за <tex>M</tex>. Найдем количество преобразований сделанных в течение <tex>\mathrm{expose(u)}</tex>. Пусть <tex>H</tex> {{---}} множество всех тяжелых ребер, <tex>L</tex> {{---}} все легкие ребра, <tex>S \rightarrow D</tex> {{---}} множество сплошных ребер, преобразованных в пунктирные в течение одного <tex>\mathrm{expose}</tex>, <tex>D \rightarrow S</tex> {{---}} множество пунктирных ребер, преобразованных в сплошные. |
<tex>M = |\{D \rightarrow S\}| = |\{L \cap D \rightarrow S\}| + |\{H \cap D \rightarrow S\}|</tex> | <tex>M = |\{D \rightarrow S\}| = |\{L \cap D \rightarrow S\}| + |\{H \cap D \rightarrow S\}|</tex> | ||
Строка 141: | Строка 141: | ||
Докажем, что [[Амортизационный анализ|амортизационная стоимость операции]] <tex>\mathrm{expose}</tex> равна <tex>O(\log n)</tex>. | Докажем, что [[Амортизационный анализ|амортизационная стоимость операции]] <tex>\mathrm{expose}</tex> равна <tex>O(\log n)</tex>. | ||
− | Пусть <tex>s(v)</tex> {{---}} количество вершин в поддеревьях <tex>v</tex> ( здесь имеется в виду splay-дерево пути, | + | Пусть <tex>s(v)</tex> {{---}} количество вершин в поддеревьях <tex>v</tex> ( здесь имеется в виду splay-дерево пути, который строится в ходе выполнения <tex>\mathrm{expose}</tex>), <tex>r(v) = \log s(v)</tex>. По [[Splay-дерево#Lemma1|лемме]] стоимость ''i''-той операции <tex>\mathrm{splay}</tex> не превосходит <tex>1 + 3 \cdot (r(t) - r(v))</tex>. Это приводит к тому, что амортизационная стоимость <tex>\mathrm{expose}</tex> ограничена следующим значением: |
<tex>3 \cdot \log n - 3 \cdot \log (s(v)) + M</tex> | <tex>3 \cdot \log n - 3 \cdot \log (s(v)) + M</tex> |
Текущая версия на 19:05, 4 сентября 2022
Динамические деревья (англ.dynamic tree) используются в двух областях: потоки и динамические графы. В первом случае динамические деревья позволяют построить эффективные алгоритмы для задачи о поиске максимального потока. В динамическом графе периодически происходят изменения: появляются и исчезают ребра, меняются их веса. Нужно быстро обрабатывать изменения, а также уметь проверять связность, искать диаметр. Динамические деревья являются инструментом, который позволяет легко решать эти задачи. Динамические деревья особенно эффективны, когда нужно работать с большими деревьями и большим количеством запросов link и cut.
Link-cut tree — это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции:
- — искать минимум на пути от вершины до корня,
- — прибавлять константу на пути от вершины до корня,
- — подвешивать одно дерево на другое,
- — отрезать дерево с корнем в вершине .
Среднее время выполнения каждой операции —
. Эта структура данных была придумана Робертом Тарьяном и Даниелем Слейтером в 1982 году.Содержание
Решение задачи в частном случае
Сначала рассмотрим частный случай, в котором все деревья — это пути, и мы хотим уметь:
- и — прибавлять константу и искать минимум на некотором суффиксе (то есть на пути от вершины до корня),
- — разбить один путь на два,
- — подвешивать голову одного пути к хвосту другого.
Если бы не последние две операции, то можно было бы применить дерево отрезков, сложив в него вершины в том порядке в котором они идут в пути. Но непонятно, как сливать или разрезать деревья отрезков. Эту задачу можно решить при помощи декартового дерева по неявному ключу. Также, если использовать какие-нибудь сливаемые деревья, то и реализуются просто. Осталось научиться искать минимум и прибавлять константу на пути. Для этого, как и в деревьях отрезков, будем хранить дополнительные значения в вершинах. В качестве сливаемых деревьев выберем splay-деревья, в которых ключи выбираются равными глубине вершины.
Тогда операции
будет соответствовать .соединяет голову первого пути с хвостом второго. Используем функцию , которая вызовет от хвоста второго пути и сделает первый путь правым ребенком корня , то есть теперь находится ниже, чем .
Чтобы прибавлять заданное число на пути от вершины до корня, будем в каждой вершине хранить величину
, которая равна разнице между весом вершины и весом её родителя. Для корня это значение равно весу самого корня. Поэтому вес вершины определятся следующим образом:, где — все предки вершины .
При прибавлении
на пути от вершины до корня, сначала вызывается , после чего в левом поддереве находятся вершины, которые лежат на пути к корню. Затем надо прибавить к и, чтобы сохранить веса вершин, которые находятся ниже в пути, вычесть от .
Для реализации будем хранить минимум уже для всего поддерева. Чтобы искать минимум от вершины , надо вызвать и сравнить её вес с минимумом левого поддерева, в котором теперь находятся все вершины пути кроме . Определим таким образом, чтобы сохранялся следующий инвариант: . Пусть и дети , тогда
Link-cut tree
Чтобы обобщить, разобьем дерево на множество непересекающихся путей. Каждое ребро обозначим либо сплошным, либо пунктирным. Все пути в link-cut дереве хранятся в виде splay-деревьев. Корень каждого splay-дерева хранит указатель на вершину-родителя. В дальнейшем будем называть этот указатель
.expose(u)
Ключевая операция в link-cut-деревьях —
. После её выполнения лежит на одном пути с корнем link-cut дерева и при этом становится корнем в splay-дереве получившегося пути. Для этого она поднимается вверх по link-cut дереву, и если какой-нибудь путь пересекает путь от до корня, то она его отрезает, разъединяя splay-дерево и делая соответствующее сплошное ребро пунктирным ребром.function expose(u: tree): splay(u) vu 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 минимумом в левом поддереве:function min(v: tree): int expose(v) ifmin(left(v)) + w(left(v)) < w(v) return min(left(v)) + w(left(v)) else return w(v)
link(v, u)
Если
— корень, а — вершина в другом дереве, то соединяет два дерева добавлением ребра , причем становится родителем .function link(v: tree, u: tree): 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))} parent(left(v)) null left(v) null
Оценка времени работы
Назовем ребро из
в её родителя тяжелым, если количество детей равное .Лемма: |
На пути от вершины до корня не больше легких ребер. |
Доказательство: |
Пусть | — количество вершин в дереве с корнем в вершине, в которой мы сейчас находимся. Поднимаясь по легкому ребру, увеличивается в два раза, поэтому, пройдя больше легких ребер, получим . Значит, в дереве не больше легких ребер.
Операция
осуществляется с помощью последовательности преобразований пунктирного ребра в сплошное ребро и другого сплошного ребра в пунктирное ребро. Обозначим количество таких преобразований за . Найдем количество преобразований сделанных в течение . Пусть — множество всех тяжелых ребер, — все легкие ребра, — множество сплошных ребер, преобразованных в пунктирные в течение одного , — множество пунктирных ребер, преобразованных в сплошные.
По лемме, количество легких пунктирных ребер, преобразованных в сплошные, будет не больше, чем
.Обозначим за
лес деревьев, в которых каждое ребро либо сплошное, либо пунктирное, a — лес, получившийся из после одного вызова . Определим потенциал , — увеличение после одной операции .Лемма: |
Доказательство: |
Теперь проанализируем . Используя тот факт, что начальное значение не превосходит , приходим к тому, что для деревьев с вершинами, по крайней мере за операцию , среднее на одну операцию будет не больше, чем .
Докажем, что амортизационная стоимость операции равна .
Пусть лемме стоимость i-той операции не превосходит . Это приводит к тому, что амортизационная стоимость ограничена следующим значением:
— количество вершин в поддеревьях ( здесь имеется в виду splay-дерево пути, который строится в ходе выполнения ), . По
Здесь
, поэтому амортизационная стоимость равна .Применение
LCA
C помощью link-cut-дерева можно найти наименьшего общего предка:
function lca(u: tree, v: tree): tree expose(u) expose(v) return pathparent(splay(u))
Первый вызов
построит путь от до корня. Второй пересечет этот путь в наименьшем общем предке, поэтому в splay-дереве, которому принадлежит , хранится указатель на , после он будет находиться в .