Декартово дерево
Ниже представлена реализация и примеры работы обычного декартова дерева с функциями insert
, erase
, build
.
Изучение данной структуры является промежуточным шагом в изучении более мощной её вариации - декартова дерева по неявному ключу.
Структура Node
Структура, из экземпляров которой и будет состоять дерево.
Здесь и далее key
будет означать ключ, priority
- приоритет, l
- указатель на левого потомка вершины, r
- указатель на правого потомка вершины.
Функции merge и split
Почти все функции декартова дерева выражаются через две основные: split
и merge
, поэтому сначала дадим их реализацию.
split
Функция split
принимает указатель на вершину t
и ключ x
, возвращает вершины l
и r
- указатели на корни левого и правого деревьев после разделения.
merge
Функция merge
принимает указатели на корни двух поддеревьев l
и r
, и возвращает указатель на новый корень r
.
Важно, чтобы ключи в левом поддереве были строго меньше ключей в правом поддереве.
Добавление и удаление
add
Чтобы вставить вершину a
с ключом a->key
и приоритетом a->priority
спустимся по дереву до первого элемента t
, у которого приоритет окажется больше a->priority
, новая вершина будет стоять на месте этой вершины t
. Произведём split
от этой вершины и ключа a->key
, полученные поддеревья l
и r
станут соответственно левым и правым поддеревом новой вершины.
remove
Чтобы удалить вершину с ключом key
также рекурсивно спустимся до вершины t
с соответствующим ключом, затем выполним merge
для её левого и правого поддерева. После merge
не забудем удалить вершину.
Построение дерева
Самый простой вариант построения - просто вызвать функцию add
для каждого элемента, однако в худшем случае это может занять квадратичное время (например если ключи и приоритеты совпадают и поступают в порядке возрастания).
Линейное построение
Линейное построение стоит применять в том случае, если вы не можете контролировать приоритеты и возможно вырождение дерева в цепочку. Стоит также заметить, что линейное построение, приведённое ниже, работает только с данными, отсортированными по ключам.
Пусть ключи отсортированы по возрастанию, будем строить дерево слева направо, при это поддерживая информацию о самом правом (последнем добавленном) элементе, а также о пути от него до вершины. Для этого удобно использовать стек. При добавлении новой вершины будем пытаться сделать её правым потомком самой правой вершины (если это позволяет приоритет). В противном случае будем подниматься по дереву, пока, либо не найдём подходящую вершину, либо не дойдём до корня. Как только мы найдём подходящую вершину, добавляемая вершина станет её правым потомком, а её бывшее правое поддерево станет левым поддеревом новой вершины.
Легко заметить, что каждая вершина ровно однажды попадает в стек и ровно однажды оттуда выталкивается, следовательно сложность алгоритма линейно зависит от количества вершин.
Выбор приоритета
Выше было замечено, что в худшем случае декартово дерево может вырождаться в цепочку. Этого можно избегать, если у нас есть возможность самостоятельно выбирать приоритеты. Если приоритеты являются случайными величинами с равномерным распределением, то можно показать, что средняя глубина вершины будет порядка логарифма от количества вершин [1].
Хороший рандом в C++
Как было показано в [2], встроенный rand()
может подвести в самый неожиданный момент.
Вместо него можно использовать следующие варианты:
или
Leave a Comment
Your email address will not be published. Required fields are marked *