Link-Cut Tree

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

Link-cut tree (dinamic-tree) — это структура данных, которая хранит лес деревьев и позволяет выполнять следующие операции:

  • min(v) — искать минимум на пути от вершины до корня;
  • add(v, c) — прибавлять константу на пути от вершины до корня;
  • link(u, w) — подвешивать одно дерево на другое;
  • cut(v) — отрезать дерево с корнем в вершине v.

Среднее время выполнения каждой операции - [math]O(log(n))[/math]. Эта структура данных была придумана Робертом Тарьяном и Даниелем Слейтером в 1982 году.

Решение задачи в частном случае

Сначала рассмотрим частный случай, в котором все деревья — это пути, и мы хотим уметь:

Пример построения дерева для пути
  • прибавлять константу и искать минимум на некотором суффиксе (то есть на пути от вершины до корня),
  • разбить один путь на два,
  • подвешивать голову одного пути к хвосту другого.

Если бы не последние две операции, то можно было бы применить дерево отрезков, сложив в него вершины в том порядке в котором они идут в пути. Но непонятно, как сливать или разрезать деревья отрезков. Если использовать какие-нибудь сливаемые деревья, то [math]link[/math] и [math]cut[/math] реализуются просто, осталось научиться искать минимум и прибавлять константу на пути. Для этого, как и в деревьях отрезков, будем хранить дополнительные значения в вершинах. В качестве сливаемых деревьев выберем splay-деревья, в которых ключи выбираются равными глубине вершины.

Тогда операции [math]cut[/math] будет соответствовать [math]split[/math].

[math]link(path1, path2)[/math] соединяет голову первого пути с хвостом второго. Используем функцию [math]merge(path2, path1)[/math], которая вызовет [math]splay[/math] от хвоста второго пути и сделает первый путь правым ребенком корня [math]path2[/math], то есть теперь [math]path1[/math] находится ниже, чем [math]path2[/math].

Чтобы прибавлять заданное число на пути от вершины до корня, будем в каждой вершине хранить величину [math]\Delta w[/math], которая равна разнице между весом вершины и весом её родителя. Для корня это значение равно весу самого корня. Поэтому вес вершины определятся следующим образом:

[math]w(u) = \sum _{v} \Delta w(v)[/math], где сумма берется по всем предкам [math]u[/math], включая саму вершину.

При прибавлении [math]\alpha[/math] на пути от вершины [math]v[/math] до корня, сначала вызывается [math]splay(v)[/math], после чего в левом поддереве находятся вершины, которые лежат на пути к корню. Затем надо прибавить [math]\alpha[/math] к [math]\Delta w(v)[/math] и, чтобы сохранить веса вершин, которые находятся ниже в пути, вычесть [math]\alpha[/math] от [math]\Delta w(right(v))[/math].

Linkcut weights.png

Для реализации [math]min[/math] будем хранить минимум уже для всего поддерева. Чтобы искать минимум от вершины [math]v[/math], надо вызвать [math]splay(v)[/math] и сравнить её вес с минимумом левого поддерева, в котором теперь находятся все вершины пути кроме [math]v[/math]. Определим [math]\Delta min(v)[/math] таким образом, чтобы сохранялся следующий инвариант: [math]min(v) = \Delta min(v) + w(v) [/math]. Пусть [math]l[/math] и [math]r[/math] дети [math]v[/math], тогда

[math]min(v) = min\{w(v), \ min(l), \ min(r)\}[/math]

[math]\Delta min(v) = min(v) - w(v) \\ = min\{w(v) - w(v), \ min(l) - w(v), \ min(r) - w(v)\} \\ = min\{0, \ (\Delta min(l) + w(l)) - w(v), \ (\Delta min(r) + w(r)) - w(v)\} \\ = min\{0, \ \Delta min(l) + \Delta w(l), \ \Delta min(r) + \Delta w(r)\}[/math]

Link-cut tree

Чтобы обобщить, разобьем дерево на множество непересекающихся путей. Каждое ребро обозначим либо solid-ребром, либо dashed-ребром. Все пути в link-cut дереве хранятся в виде splay-деревьев. Корень каждого splay-дерева хранит указатель на вершину-родителя. В дальнейшем будем называть этот указатель [math]pathparent[/math].

Разбиение дерева на пути

expose(u)

Ключевая операция в link-cut-деревьях — [math]expose(u)[/math]. После её выполнения [math]u[/math] лежит на одном пути с корнем link-cut дерева и при этом становится корнем в splay-дереве получившегося пути. Для этого она поднимается вверх по link-cut дереву, и если какой-нибудь путь пересекает путь от [math]u[/math] до корня, то она его отрезает, разъединяя splay-дерево и делая соответствующее solid-ребро dashed-ребром.

Разбиение дерева на пути
function expose(u : tree):
    splay(u)
    v [math]\leftarrow[/math] u
    while v != root
        p [math]\leftarrow[/math] pathparent(v)        //получаем указатель на ближайшую вершину пути, пересекающего путь от u до корня
        splay(p)                  //теперь в правом поддереве p находятся вершины пути, которые находятся ниже чем p в link-cut-дереве,
        parent(right(p)) [math]\leftarrow[/math] null  //поэтому правое поддерево p делаем новым путем
        pathparent(right(p)) [math]\leftarrow[/math] p
        right(p) [math]\leftarrow[/math] v             //объединяем оставшийся и построенный пути
        [math]\vartriangle[/math]w(v) -= [math]\vartriangle[/math]w(p)
        [math]\vartriangle[/math]min(p) [math]\leftarrow[/math] min{0, [math]\vartriangle[/math]min(left(p)) + [math]\vartriangle[/math]w(left(p)), [math]\vartriangle[/math]min(right(p)) + [math]\vartriangle[/math]w(right(p))}
        pathparent(v) [math]\leftarrow[/math] null
        v [math]\leftarrow[/math] p
    splay(u)

add(v, c)

Чтобы прибавить константу на пути от [math]v[/math] до корня link-cut-дерева вызовем [math]expose(v)[/math], что построит запрашиваемый путь в виде splay-дерева, в котором [math]v[/math] - корень, и в левом поддереве находятся вершины, которые находятся выше чем [math]v[/math] в link-cut-дереве (то есть все вершины пути без [math]v[/math]), а в правом - те, что ниже. Тогда прибавим [math]c[/math] к [math]\Delta w(v)[/math] и вычтем константу от правого ребенка [math]v[/math], чтобы скомпенсировать разницу и сохранить инвариант.

function add(v : tree, c : int):
    expose(v)
    [math]\vartriangle[/math]w(v) += c
    [math]\vartriangle[/math]w(right(v)) -= c

min(v)

Построим splay-дерево для пути и сравним минимум корня [math]v[/math] c минимумом в левом поддереве:

int min(v : tree):
    expose(v)
    if [math]\vartriangle[/math]min(left(v)) + [math]\vartriangle[/math]w(left(v)) < [math]\vartriangle[/math]w(v)
        return [math]\vartriangle[/math]min(left(v)) + [math]\vartriangle[/math]w(left(v))
    else
        return [math]\vartriangle[/math]w(v)

link(v, u)

Если [math]v[/math] - корень, а [math]u[/math] - вершина в другом дереве, то [math]link(v, u)[/math] соединяет два дерева добавлением ребра [math](v, u)[/math], причем [math]u[/math] становится родителем [math]v[/math].

function link(v : tree, u : tree):
    expose(v)      //теперь v - корень в splay-дереве пути и не имеет левого ребенка(так как ключ равен глубине в link-cut дереве)
    expose(u)
    [math]\vartriangle[/math]w(u) -= [math]\vartriangle[/math]w(v) //чтобы сделать u родителем v в link-cut дереве 1. делаем путь, содержащий u, левым ребенком v в splay-дереве
    parent(u) [math]\leftarrow[/math] v //                                              2. обновляем [math]\vartriangle[/math]w, [math]\vartriangle[/math]min
    left(v) [math]\leftarrow[/math] u
    [math]\vartriangle[/math]min(v) [math]\leftarrow[/math] min{0, [math]\vartriangle[/math]min(u) + [math]\vartriangle[/math]w(u), [math]\vartriangle[/math]min(right(v)) + [math]\vartriangle[/math]w((right(v)))}

cut(v)

Отрезает дерево с корнем [math]v[/math]. После вызова [math]expose(v) \ v[/math] станет корнем splay-дерева, и в правом поддереве будут содержатся все вершины, которые были ниже [math]v[/math] в link-cut дереве, а в левом - те что выше. Обнулив указатель на левого ребенка [math]v[/math] и на родителя в левом поддереве, получим требуемое.

function cut(v : tree):
    expose(v)
    [math]\vartriangle[/math]w(left(v)) += [math]\vartriangle[/math]w(v)
    [math]\vartriangle[/math]min(v) [math]\leftarrow[/math] min{0, [math]\vartriangle[/math]min(right(v)) + [math]\vartriangle[/math]w(right(v))}
    left(v) [math]\leftarrow[/math] null
    parent(left(v)) [math]\leftarrow[/math] null

Оценка времени работы

Назовем ребро из [math]u[/math] в её родителя [math]v[/math] тяжелым, если количество детей [math]u[/math] [math]d(u) \gt \frac{1}{2} d(v)[/math].

Лемма:
На пути от вершины до корня не больше [math]log(n)[/math] легких ребер.
Доказательство:
[math]\triangleright[/math]
Пусть [math]m[/math] — количество вершин в дереве с корнем в вершине, в которой мы сейчас находимся. Поднимаясь по легкому ребру, [math]m[/math] увеличивается в два раза, поэтому, пройдя больше [math]\log (n)[/math] легких ребер, получим [math]m \gt n[/math]. Значит, в дереве не больше [math]\log (n)[/math] легких ребер.
[math]\triangleleft[/math]

Операция [math]u[/math] осуществляется с помощью последовательности преобразований dashed-ребра в solid-ребро и другого solid-ребра в dashed-ребро. Обозначим количество таких преобразований за [math]M[/math]. Найдем количество преобразований сделанных в течение [math]expose(u)[/math]. Пусть [math]H[/math] - множество всех тяжелых ребер, [math]L[/math] - все легкие ребра, [math]S \rightarrow D[/math] - множество solid-ребер, преобразованных в dashed в течение одного [math]expose[/math], [math]D \rightarrow S[/math] - множество dashed-ребер, преобразованных в solid.

[math]M = |\{D \rightarrow S\}| = |\{L \cap D \rightarrow S\}| + |\{H \cap D \rightarrow S\}|[/math]

По лемме, количество легких dashed-ребер, преобразованных в solid, будет не больше, чем [math]\log n[/math].

Обозначим за [math]F[/math] лес деревьев, в которых каждое ребро либо solid, либо dashed, a [math]F'[/math] — лес, получившийся из [math]F[/math] после одного вызова [math]expose[/math]. Определим потенциал [math]\Phi _{a}(F) = n - 1 - |\{H \cap solid-edges\}|[/math], [math]\Delta \Phi _{a}[/math] — увеличение [math]\Phi _{a}[/math] после одной операции [math]expose[/math].

Лемма:
[math]V = M + \Delta \Phi _{a} \leqslant 1 + 2\log n [/math]
Доказательство:
[math]\triangleright[/math]
[math]V = M + \Delta \Phi _{a}\\ = M + |H \cap S \rightarrow D| - |H \cap D \rightarrow S| \\ \leqslant M + |L \cap D \rightarrow S| - |H \cap D \rightarrow S| \\ = 2 \times |L \cap D \rightarrow S| \\ =2 \times \log n [/math]
[math]\triangleleft[/math]


Теперь проанализируем [math]M[/math]. Используя тот факт, что начальное значение [math]\Phi _{a}[/math] не превосходит [math]n - 1[/math], приходим к тому, что для деревьев с [math]n[/math] вершинами, по крайней мере за [math]n - 1[/math] операцию [math]expose[/math], среднее [math]M[/math] на одну операцию будет не больше, чем [math]1 + 2\log n[/math]

Докажем, что амортизационная стоимость операции [math]expose[/math] равна [math]O(log(n))[/math]

Пусть [math]s(v)[/math] — количество вершин в поддеревьях [math]v[/math] ( здесь имеется в виду splay-дерево пути, котоый строится в ходе выполнения [math]expose[/math]), [math]r(v) = log(s(v))[/math]. По лемме стоимость i-той операции [math]splay[/math] не превосходит [math]1 + 3 \times (r(t) - r(v))[/math]. Это приводит к тому, что амортизационная стоимость [math]expose[/math] ограничена следующим значением:

[math]3 \times \log n - 3 \times \log (s(v)) + M[/math]

Здесь [math]M = O(\log n)[/math], поэтому амортизационная стоимость [math]expose[/math] равна [math]O(\log n)[/math]

Применение

LCA

C помощью link-cut-дерева можно найти наименьшего общего предка:

lca(u, v)
    expose(u)
    expose(v)
    return pathparent(splay(u))

Первый вызов [math]expose[/math] построит путь от [math]u[/math] до корня. Второй пересечет этот путь в наименьшем общем предке, поэтому в splay-дереве, которому принадлежит [math]u[/math], хранится указатель [math]pathparent[/math] на [math]lca[/math], после [math]splay(u)[/math] он будет находиться в [math]u[/math].

Ссылки