Фибоначчиева куча
Фибоначчиева куча (англ. 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 = R 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