Декартово дерево

Материал из Викиконспекты
Версия от 11:32, 26 ноября 2016; 37.57.17.214 (обсуждение) (Другой алгоритм за O(n\log n): опечатка)
Перейти к: навигация, поиск

Эта статья про курево

Декартово дерево или дерамида (англ. Treap) — это структура данных, объединяющая в себе бинарное дерево поиска и бинарную кучу (отсюда и второе её название: treap (tree + heap) и дерамида (дерево + пирамида), также существует название курево (куча + дерево).

Более строго, это бинарное дерево, в узлах которого хранится пары (x,y), где x — это ключ, а y — это приоритет. Также оно является двоичным деревом поиска по x и пирамидой по y. Предполагая, что все x и все y являются различными, получаем, что если некоторый элемент дерева содержит (x0,y0), то у всех элементов в левом поддереве x<x0, у всех элементов в правом поддереве x>x0, а также и в левом, и в правом поддереве имеем: y<y0.

Дерамиды были предложены Сиделем (Siedel) и Арагоном (Aragon) в 1996 г.

Операции в декартовом дереве

split

Операция split

Операция split (разрезать) позволяет сделать следующее, разрезать исходное дерево T по ключу k. Возвращать она будет такую пару деревьев T1,T2, что в дереве T1 ключи меньше k, а в дереве T2 все остальные: split(T,k)T1,T2.

Эта операция устроена следующим образом.

Рассмотрим случай, в котором требуется разрезать дерево по ключу, большему ключа корня. Посмотрим, как будут устроены результирующие деревья T1 и T2:

  • T1: левое поддерево T1 совпадёт с левым поддеревом T. Для нахождения правого поддерева T1, нужно разрезать правое поддерево T на TR1 и TR2 по ключу k и взять TR1.
  • T2 совпадёт с TR2.

Случай, в котором требуется разрезать дерево по ключу, меньше либо равному ключа в корне, рассматривается симметрично.

Псевдокод

Treap, Treap split(t: Treap, k: int):
  if t == 
    return ,      
  else if k > t.x 
    t1, t2 = split(t.right, k)
    t.right = t1
    return t, t2
  else 
    t1, t2 = split(t.left, k)
    t.left = t2
    return t1, t

Время работы

Оценим время работы операции split. Во время выполнения вызывается одна операция split для дерева хотя бы на один меньшей высоты и делается ещё O(1) операций. Тогда итоговая трудоёмкость этой операции равна O(h), где h — высота дерева.

merge

Операция merge

Рассмотрим вторую операцию с декартовыми деревьями — merge (слить).

С помощью этой операции можно слить два декартовых дерева в одно. Причем, все ключи в первом(левом) дереве должны быть меньше, чем ключи во втором(правом). В результате получается дерево, в котором есть все ключи из первого и второго деревьев: merge(T1,T2){T}

Рассмотрим принцип работы этой операции. Пусть нужно слить деревья T1 и T2. Тогда, очевидно, у результирующего дерева T есть корень. Корнем станет вершина из T1 или T2 с наибольшим приоритетом y. Но вершина с самым большим y из всех вершин деревьев T1 и T2 может быть только либо корнем T1, либо корнем T2. Рассмотрим случай, в котором корень T1 имеет больший y, чем корень T2. Случай, в котором корень T2 имеет больший y, чем корень T1, симметричен этому.

Если y корня T1 больше y корня T2, то он и будет являться корнем. Тогда левое поддерево T совпадёт с левым поддеревом T1. Справа же нужно подвесить объединение правого поддерева T1 и дерева T2.

Псевдокод

Treap merge(t1: Treap, t2: Treap):
  if t2 == 
    return t1
  if t1 == 
    return t2
  else if t1.y > t2.y
    t1.right = merge(t1.right, t2)
    return t1
  else 
    t2.left = merge(t1, t2.left)
    return t2

Время работы

Рассуждая аналогично операции split приходим к выводу, что трудоёмкость операции merge равна O(h), где h — высота дерева.

insert

Операция insert(T,k) добавляет в дерево T элемент k, где k.x — ключ, а k.y— приоритет.

Представим что элемент k, это декартово дерево из одного элемента, и для того чтобы его добавить в наше декартово дерево T, очевидно, нам нужно их слить. Но T может содержать ключи как меньше, так и больше ключа k.x, поэтому сначала нужно разрезать T по ключу k.x.

  • Реализация №1
  1. Разобьём наше дерево по ключу, который мы хотим добавить, то есть split(T,k.x)T1,T2.
  2. Сливаем первое дерево с новым элементом, то есть merge(T1,k)T1.
  3. Сливаем получившиеся дерево со вторым, то есть merge(T1,T2)T.
  • Реализация №2
  1. Сначала спускаемся по дереву (как в обычном бинарном дереве поиска по k.x), но останавливаемся на первом элементе, в котором значение приоритета оказалось меньше k.y.
  2. Теперь вызываем split(T,k.x)T1,T2 от найденного элемента (от элемента вместе со всем его поддеревом)
  3. Полученные T1 и T2 записываем в качестве левого и правого сына добавляемого элемента.
  4. Полученное дерево ставим на место элемента, найденного в первом пункте.

В первой реализации два раза используется merge, а во второй реализации слияние вообще не используется.

remove

Операция remove(T,x) удаляет из дерева T элемент с ключом x.

  • Реализация №1
  1. Разобьём наше дерево по ключу, который мы хотим удалить, то есть split(T,k.x)T1,T2.
  2. Теперь отделяем от первого дерева элемент x, то есть самого левого ребенка дерева T2.
  3. Сливаем первое дерево со вторым, то есть merge(T1,T2)T.
  • Реализация №2
  1. Спускаемся по дереву (как в обычном бинарном дереве поиска по x), и ищем удаляемый элемент.
  2. Найдя элемент, вызываем merge его левого и правого сыновей
  3. Результат процедуры merge ставим на место удаляемого элемента.

В первой реализации один раз используется split, а во второй реализации разрезание вообще не используется.

Построение декартова дерева

Пусть нам известно из каких пар (xi,yi) требуется построить декартово дерево, причем также известно, что x1<x2<<xn.

Алгоритм за O(nlogn)

Отсортируем все приоритеты по убыванию за O(nlogn) и выберем первый из них, пусть это будет yk. Сделаем (xk,yk) корнем дерева. Проделав то же самое с остальными вершинами получим левого и правого сына (xk,yk). В среднем высота Декартова дерева logn (см. далее) и на каждом уровне мы сделали O(n) операций. Значит такой алгоритм работает за O(nlogn).


Другой алгоритм за O(nlogn)

Отсортируем парочки (xi,yi) по убыванию xi и положим их в очередь. Сперва достанем из очереди первые 2 элемента и сольем их в дерево и положим в конец очереди, затем сделаем то же самое со следующими двумя и т.д. Таким образом, мы сольем сначала n деревьев размера 1, затем n2 деревьев размера 2 и так далее. При этом на уменьшение размера очереди в два раза мы будем тратить суммарно O(n) время на слияния, а всего таких уменьшений будет logn. Значит полное время работы алгоритма будет O(nlogn).

Алгоритм за O(n)

Будем строить дерево слева направо, то есть начиная с (x1,y1) по (xn,yn), при этом помнить последний добавленный элемент (xk,yk). Он будет самым правым, так как у него будет максимальный ключ, а по ключам декартово дерево представляет собой двоичное дерево поиска. При добавлении (xk+1,yk+1), пытаемся сделать его правым сыном (xk,yk), это следует сделать если yk>yk+1, иначе делаем шаг к предку последнего элемента и смотрим его значение y. Поднимаемся до тех пор, пока приоритет в рассматриваемом элементе меньше приоритета в добавляемом, после чего делаем (xk+1,yk+1) его правым сыном, а предыдущего правого сына делаем левым сыном (xk+1,yk+1).


Заметим, что каждую вершину мы посетим максимум дважды: при непосредственном добавлении и, поднимаясь вверх (ведь после этого вершина будет лежать в чьем-то левом поддереве, а мы поднимаемся только по правому). Из этого следует, что построение происходит за O(n).

Случайные приоритеты

Мы уже выяснили, что сложность операций с декартовым деревом линейно зависит от его высоты. В действительности высота декартова дерева может быть линейной относительно его размеров. Например, высота декартова дерева, построенного по набору ключей (1,1),,(n,n), будет равна n. Во избежание таких случаев, полезным оказывается выбирать приоритеты в ключах случайно.

Высота в декартовом дереве с случайными приоритетами

Теорема:
В декартовом дереве из n узлов, приоритеты y которого являются случайными величинами c равномерным распределением, средняя глубина вершины O(logn).
Доказательство:

Будем считать, что все выбранные приоритеты y попарно различны.

Для начала введем несколько обозначений:

  • xk — вершина с k-ым по величине ключом;
  • индикаторная величина Ai,j={1,xi is ancestor of xj0,otherwise
  • d(v) — глубина вершины v;

В силу обозначений глубину вершины можно записать как количество предков:

d(xk)=ni=1Ai,k.

Теперь можно выразить математическое ожидание глубины конкретной вершины:

E(d(xk))=ni=1Pr[Ai,k=1] — здесь мы использовали линейность математического ожидания, и то что E(X)=Pr[X=1] для индикаторной величины X (Pr[A] — вероятность события A).

Для подсчёта средней глубины вершин нам нужно сосчитать вероятность того, что вершина xi является предком вершины xk, то есть Pr[Ai,k=1].

Введем новое обозначение:

  • Xi,k — множество ключей {xi,,xk} или {xk,,xi}, в зависимости от i<k или i>k. Xi,k и Xk,i обозначают одно и тоже, их мощность равна |ki|+1.
Лемма:
Для любых ik , xi является предком xk тогда и только тогда, когда xi имеет наибольший приоритет среди Xi,k.
Доказательство:

Если xi является корнем, то оно является предком xk и по определению имеет максимальный приоритет среди всех вершин, следовательно, и среди Xi,k.

С другой стороны, если xk — корень, то xi — не предок xk, и xk имеет максимальный приоритет в декартовом дереве; следовательно, xi не имеет наибольший приоритет среди Xi,k.

Теперь предположим, что какая-то другая вершина xm — корень. Тогда, если xi и xk лежат в разных поддеревьях, то i<m<k или i>m>k, следовательно, xm содержится в Xi,k. В этом случае xi — не предок xk, и наибольший приоритет среди Xi,k имеет вершина с номером m.

Наконец, если xi и xk лежат в одном поддереве, то доказательство применяется по индукции: пустое декартово дерево есть тривиальная база, а рассматриваемое поддерево является меньшим декартовым деревом.

Так как распределение приоритетов равномерное, каждая вершина среди Xi,k может иметь максимальный приоритет, мы немедленно приходим к следующему равенству:

Pr[Ai,j=1]={1ki+1,k > i0,k = i1ik+1,k < i

Подставив последнее в нашу формулу с математическим ожиданием получим:

E(d(xk))=ni=1Pr[Ai,k=1]=k1i=11ki+1+ni=k+11ik+1

ln(k)+ln(nk)+2 (здесь мы использовали неравенство ni=11iln(n)+1)

log(n) отличается от ln(n) в константу раз, поэтому log(n)=O(ln(n)).
В итоге мы получили что E(d(xk))=O(log(n)).

Таким образом, среднее время работы операций split и merge будет O(log(n)).

См. также

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