Фибоначчиева куча
Фибоначчиева куча (англ. Fibonacci heap) — структура данных, отвечающая интерфейсу приоритетная очередь. Эта структура данных имеет меньшую амортизированную сложность, чем такие приоритетные очереди как биномиальная куча и двоичная куча. Изначально эта структура данных была разработана Майклом Фридманом[1] и Робертом Тарьяном[2] при работе по улучшению асимптотической сложности алгоритма Дейкстры. Свое название Фибоначчиева куча получила из-за использования некоторых свойств чисел Фибоначчи[3] в потенциальном анализе этой реализации.
Содержание
[убрать]Структура
Фибоначчиева куча — набор из подвешенных деревьев удовлетворяющих свойству: каждый предок не больше своих детей(если дерево на минимум). Это означает, что минимум всей кучи это один из корней этих деревьев. Одним из главных преимуществ Фибоначчиевой кучи — гибкость её структуры из-за того, что на деревья не наложены никакие ограничения по форме. Например, Фибоначчиева куча может состоять хоть из деревьев в каждом из которых по одному элементу. Такая гибкость позволяет выполнять некоторые операции лениво, оставляя работу более поздним операциям. Далее будут даны некоторые определения, которые понадобятся в дальнейшем.
Определение: |
Степень вершины (англ. degree) — количество детей данной вершины. Далее будем обозначать как | , где это вершина.
Определение: |
Степень кучи (англ. degree) — наибольшая степень вершины этой кучи. Далее будем обозначать как | , где это куча.
Реализация
Для возможности быстрого удаления элемента из произвольного места и объединением с другим списком будем хранить их в циклическом двусвязном списке. Также будем хранить и все уровни поддерева. Исходя из этого структура каждого узла будет выглядеть вот так.
struct Node
int key // ключ
Node parent // указатель на родительский узел
Node child // указатель на один из дочерних узлов
Node left // указатель на левый узел того же предка
Node right // указатель на правый узел того же предка
int degree // степень вершины
boolean mark // был ли удален в процессе изменения ключа ребенок этой вершины)
Также стоит упомянуть, что нам нужен указатель только на одного ребенка, поскольку остальные хранятся в двусвязном списке с ним. Для доступа ко всей куче нам тоже нужен всего один элемент, поэтому разумно хранить именно указатель на минимум кучи(он обязательно один из корней), а для получения размера за константное время будем хранить размер кучи отдельно.
struct fibonacciHeap
int size // текущее количество узлов
Node min // указатель на корень дерева с минимальным ключом
Cоздание кучи
Инициализация кучи.
function buildHeap:
min
size = 0
Вставка элемента
Данная операция вставляет новый элемент в список корней правее минимума и при необходимости меняет указатель на минимум кучи.
function insert(x: int):
Node newNode // создаем новый узел
newNode.key = x // инициализируем ключ нового узла
if size = 0 // если куче нет элементов, то только что добавленный минимальный
min = newNode
min.left = newNode
min.right = newNode
else // иначе аккуратно меняем указатели в списке, чтобы не перепутать указатели
Node prevRight = min.right
min.right = newNode
newNode.left = min
newNode.right = prevRight
prevRight.left = newNode
if newNode.key < min.key
min = newNode // меняем указатель на минимум, если надо
newNode.parent
size++ // не забываем увеличить переменную size
Получение минимального элемента
Получение минимума всей кучи.
int getMin:
return min.key
Соедининение двух куч
Для сливание двух Фибоначчиевых куч необходимо просто объединить их корневые списки, а также обновить минимум новой кучи, если понадобится. Вынесем в вспомогательную функцию
function unionLists(first: Node, second: Node):
Node L = first.left // аккуратно меняем указатели местами указатели
Node R = second.right
second.right = first
first.left = second
L.right = R
R.left = L
Сливаем два корневых списка в один и обновляем минимум, если нужно.
function merge(that: fibonacciHeap):
if that.size = 0 // если вторая куча пуста, нечего добавлять
return
if size = 0 // если наша куча пуста, то результатом будет вторая куча
min = that.min
size = that.size
else
unionLists(min, that.min) // объединяем два корневых списка
size += that.size
if min or (that.min and that.min < min) // если минимум кучи изменился, то надо обновить указатель
min = that.min
Удаление минимального элемента
Первая рассматриваемая операция, в ходе которой значительно меняется структура кучи. Здесь используется вспомогательная процедура
int deleteMin:
Node prevMin = min
unionLists(min, min.child) // список детей min объединяем с корневым
Node L = min.left // аккуратно удаляем min из списка
Node R = min.right
L.right = R
R.left = L
if prevMin.right = prevMin // отдельно рассмотрим случай с одним элементом
min
return
min = min.right // пока что перекинем указаиель min на правого сына, а далее consolidate() скорректирует min в процессе выполнения
consolidate()
size--
return prevMin.key
Прорежение деревьев
Данная процедура принимает кучу и преобразует ее таким образом, что в корневом списке остается не более
вершин.Для этого возьмем массив списков указателей на корни деревьев
, где — максимальная степень вершины в текущем корневом списке.Затем происходит процесс, аналогичный слиянию биномиальных куч: добавляем поочередно каждый корень, смотря на его степень. Пусть она равна . Если в соответствующей ячейке еще нету вершины, записываем текущую вершину туда. Иначе подвешиваем одно дерево к другому, и пытаемся также добавить дерево, степень корня которого уже равна . Продолжаем, пока не найдем свободную ячейку. Подвешиваем мы его следующим образом: в корневой список добавляем корень минимальный из тех двух, а корень другого добавляем в список детей корневой вершины. Чтобы лучше понять этот процесс лучше воспользоваться визуализатором
function consolidate:
A = Node[]
A[min.degree] = min // создаем массив и инициализируем его min
Node current = min.right
while A[current.degree] current // пока элементы массива меняются
if A[current.degree] // если ячейка пустая, то положим в нее текущий элемент
A[current.degree] = current
current = current.right
else // иначе подвесим к меньшему из текущего корня и того, который лежит в ячейке другой
Node conflict = A[current.degree]
Node addTo, adding
if conflict.key < current.key
addTo = conflict
adding = current
else
addTo = current
adding = conflict
unionLists(addTo.child, adding)
adding.parent = addTo
addTo.degree++
current = addTo
if min.key > current.key // обновляем минимум, если нужно
min = current
Пример
Изначально добавляем в нашу кучу
элементов . После этого выполним операцию извлечения минимума:
- Удалим минимальный элемент из циклического корневого списка и заведем массив для дальнейшего прорежения.
- Начнем процесс протяжения с первого элемента — . Его степень равна поэтому запишем его адрес в нулевую ячейку массива.
- Следующий элемент тоже имеет степень . Возникает конфликт, который решается подвешиванием к меньшему корню большего. То есть к подвешиваем и увеличиваем степень на . В итоге степень равна . Записываем адрес по индексу в массив.
- Делаем тоже самое, что и на предыдущих итерациях, но теперь объединяем и
- Теперь у нас два элемента со степенью в корневом списке. Объединим их подвесив к меньшему корню — , больший — . Теперь степень равна , запишем на позицию массива обновленное значение.
- Ну и наконец аналогично объедений последние два элемента.
Уменьшение значения элемента
Основная идея: хотим, чтобы учетная стоимость данной операции была список. Итак, сам алгоритм:
. Было бы хорошо, чтобы вершина не всплывала до корня, и тогда дерево не придется сильно перестраивать. Для этого при удобном случае будем вырезать поддерево полностью и перемещать его в корневой- Проверяем, если новое значение ключа все же не меньше значения ключа родителя, то все хорошо, и мы выходим.
- Иначе, вырезаем дерево с текущей вершиной в корневой список, и производим каскадное вырезание родителя.
function decreaseKey(x: Node, newValue: int):
if newValue > x.parent.key // если после изменения структкра дерева сохранится, то меняем и выходим
x.key = newValue
return
Node parent = x.parent // иначе вызываем cut и cascadingCut
cut(x)
cascadingCut(x.parent)
Вырезание
При вырезании вершины мы удаляем ее из списка детей своего родителя, уменьшаем степень ее родителя (
function cut(x: Node)
Node L = x.left
Node R = x.right
R.left = L // аккуратно удаляем текущую вершину
L.right = R
x.right = x
x.left = x
x.parent.degree--
if x.parent.child = x // чтобы родитель не потерял ссылку на сыновей проверяем:
if x.right = x. // если узел который мы вырезаем содержится в родителе, то меняем его на соседний
x.parent.child // иначе у родителя больше нет детей
else
x.parent.child = x.right
x.parent
unionLists(min, x) // вставляем наше поддерево в корневой список
Каскадное вырезание
Перед вызовом каскадного вырезания нам известно, удаляли ли ребенка у этой вершины. Если у вершины до этого не удаляли дочерний узел (
), то мы помечаем эту вершину ( ) и прекращаем выполнение операции. В противном случае применяем операцию для текущей вершины и запускаем каскадное вырезание от родителя.
function cascadingCut(x: Node)
while x.mark = true // пока у нас помеченые вершины вырезаем их
cut(x)
x = x.parent
x.mark = true // последнюю вершину нужно пометить — у нее удаляли ребенка
Пример
Рисунок иллюстрирует пример каскадного вырезания:
- Изначально, куча состояла из фибоначчиевых деревьев. У вершины с ключом отсутствует ребенок.
- Уменьшаем ключ список и помечаем ее родителя. до и делаем операцию этого дерева. Получаем кучу с деревьями и новым минимумом. Но у вершины с ключом был удален второй ребенок, поэтому запускам операцию для этой вершины: вырезаем ее, помещаем в корневой
- У вершины с ключом удален лишь один ребенок, поэтому операция от нее не запускается. В итоге, получаем кучу, состоящую из фибоначчиевых деревьев.
Удаление элемента
Удаление вершины реализуется через уменьшение ее ключа до
function delete(x: Node)
decreaseKey(x,
)
deleteMin()
Время работы
Потенциал
Для анализа производительности операций введем потенциал для фибоначчиевой кучи как
, где — количество элементов в корневом списке кучи, а — количество вершин, у которых удален один ребенок (то есть вершин с пометкой ). Договоримся, что единицы потенциала достаточно для оплаты константного количества работы.Cоздание кучи
Очевидно, что реальное время работы —
.Вставка элемента
Для оценки амортизированной стоимости операции рассмотрим исходную кучу
и получившуюся в результате вставки нового элемента кучу . и . Следовательно, увеличение потенциала составляет . Так как реальное время работы составляет , то амортизированная стоимость данной операции также равна .Получение минимального элемента
Истинное время работы —
.Соедининение двух куч
Реальное время работы —
. Амортизированное время работы также , поскольку, при объединении двух куч в одну, потенциалы обеих куч суммируются, итоговая сумма потенциалов не изменяется, .Удаление минимального элемента
Для доказательства времени работы этого алгоритма нам понадобится доказать несколько вспомогательных утверждений.
Лемма: |
Для всех целых
, где — -ое число Фибоначчи, определяемое формулой: |
Доказательство: |
Докажем лемму по индукции: при , что действительно верно. По индукции предполагаем, что . Тогда |
Лемма: |
Фибоначчиево дерево порядка содержит не менее вершин. |
Доказательство: |
Докажем это утверждение по индукции. Пусть — минимальный размер фибоначчиева дерева порядка .При . При . Предположим по индукции, что для всех . Пусть в нашем дереве удалено поддерево порядка . Тогда
Но по предыдущей лемме : . Следовательно, |
Лемма: |
, где |
Доказательство: |
Для начала докажем, что Используем для этого математическую индукцию. При , что верно. При , что также верно. По индукции предполагаем, что и . Тогда
Подставив вместо Поскольку его значение, нетрудно убедится, что , то выполняются неравенства . Таким образом, -ое число Фибоначчи равно , округленному до ближайшего целого числа. Следовательно, . |
Лемма: |
Максимальная степень произвольной вершины в фибоначчиевой куче с вершинами равна |
Доказательство: |
Пусть доказанному выше в дереве, корень которого , содержится не менее вершин, что в свою очередь по лемме равно . То есть — произвольная вершина в фибоначчиевой куче с вершинами, и пусть — степень вершины . Тогда по
Логарифмируя по основанию , получаемТаким образом, максимальная степень произвольной вершины равна . |
Итоговая асимптотика операции лемме .
, учитывая и вспомогательную функцию , время работы которой доказывается ниже, равно: . По доказанной вышеУчетная стоимость
равна . Докажем это:Изначально в корневом списке было не более
вершин, поскольку он состоит из исходного списка корней с узлами, минус извлеченный узел и плюс дочерние узлы, количество которых не превышает . В ходе операции мы сделали слияний деревьев. Потенциал перед извлечением минимума равен , а после не превышает , поскольку в корневом списке остается не более узлов, а количество помеченных узлов не изменяется. Таким образом, амортизированная стоимость не превосходит
Поскольку мы договорились, что можем масштабировать единицу потенциала таким образом, чтобы покрывать константное количество работы, то итоговая амортизационная оценка —
Уменьшение значения элемента
Докажем, что амортизированное время работы операции
есть . Поскольку в процедуре нет циклов, ее время работы определяется лишь количеством рекурсивных вызовов каскадного вырезания.Пусть мы вызвали процедуру каскадного вырезания подверглось
раз. Так как реальное время работы каждой итерации составляет , то реальное время работы операции — .Рассмотрим, как изменится потенциал в результате выполнения данной операции. Пусть
— фибоначчиева куча до вызова . Тогда после итераций операции вершин с пометкой стало как минимум на меньше, потому что каждый вызов каскадного вырезания, за исключением последнего, уменьшает количество помеченных вершин на одну, и в результате последнего вызова одну вершину мы можем пометить. В корневом списке прибавилось новых деревьев ( дерево за счет каскадного вырезания и еще одно из-за самого первого вызова операции ).В итоге, изменение потенциала составляет:
. Следовательно, амортизированная стоимость не превышает . Но поскольку мы можем соответствующим образом масштабировать единицы потенциала, то амортизированная стоимость операции равна .Удаление элемента
Амортизированное время работы:
.Поскольку ранее мы показали, что
, то соответствующие оценки доказаны.Итоговая таблица
Операция | Амортизированная сложность |
---|---|
Недостатки и достоинства
Недостатки:
- Большое потребление памяти на узел(минимум 21 байт)
- Большая константа времени работы, что делает ее малоприменимой для реальных задач
- Некоторые операции в худшем случае могут работать за времени
Достоинства:
- Одно из лучших асимптотических времен работы для всех операций
См. также
- Приоритетные очереди
- Двоичная куча
- Биномиальная куча
- Левосторонняя куча
- Тонкая куча
- Толстая куча на избыточном счетчике
- Куча Бродала-Окасаки
Примечания
Источники информации
- Томас Кормен, Чарльз Лейзерсон, Рональд Ривест, Клиффорд Штайн — Алгоритмы: построение и анализ. — М.: Издательский дом «Вильямс», 2005. — С. 1296. — ISBN 5-8459-0857-4
- Числа Фибоначчи — Википедия
- Фибоначчиева куча — Википедия
- Fibonacci heap visualization
- Фибоначчиевы кучи — INTUIT.ru
- Fibonacci Heaps — Duke University
- Fibonacci Heaps — Princeton University