Куча Фибоначчи: ленивая структура и амортизация

Куча Фибоначчи - это «ленивая» mergeable-приоритетная очередь, предложенная Майклом Фредманом и Робертом Тарьяном в 1984 году. Её главная особенность - амортизированные оценки: insert, find-min, merge и decrease-key работают за , а extract-min и delete - за amortized. Имя «Фибоначчи» возникло не из-за вспомогательных счётчиков, а из доказательства верхней границы степени узла: размер поддерева, висящего у узла степени , ограничен снизу числом Фибоначчи , откуда .
Идея: откладываем работу до последнего момента
В биномиальной куче после каждого insert приходится поддерживать инвариант «уникальные ранги в лесу». Это даёт worst-case на вставку. Куча Фибоначчи действует ровно наоборот: insert просто добавляет новый одноузловой корень в общий двусвязный корневой список и обновляет указатель на минимум. Никаких сливаний. Структура временно становится «грязной» - деревьев одного ранга может быть сколько угодно.
Очистка (консолидация) откладывается до ближайшего extract-min. Только тогда алгоритм просматривает все корни, последовательно объединяет деревья одинаковой степени, пока в лесу не останется не более одного дерева каждой степени. Эта работа единовременно дорогая, но её стоимость размазывается на дешёвые insert и decrease-key - отсюда амортизированные .
Чем куча Фибоначчи отличается от биномиальной
Биномиальная куча - это лес строгих биномиальных деревьев , и каждое слияние выполняется немедленно. Куча Фибоначчи отказывается от строгой формы деревьев: после серии decrease-key дерево может потерять детей и стать «обрезанным», не сохраняя узлов на ранг . Зато все «ленивые» правки выполняются за константу, а реальная стоимость возникает только при extract-min.
| Свойство | Биномиальная | Фибоначчи |
|---|---|---|
insert | amort. / worst | |
find-min | или с указателем | |
extract-min | amort. | |
decrease-key | amort. | |
merge | ||
| Форма деревьев | строгие | произвольная, но ограниченная по степени |
Структура данных и пометки
Каждый узел хранит ключ, степень (число прямых детей), указатели на родителя и одного из детей, циклический двусвязный список «братьев» и булев флаг mark. Корни всех деревьев соединены в один циклический двусвязный список (root list), и отдельный указатель min всегда ссылается на корень с наименьшим ключом.
Флаг mark ставится, когда у узла потеряли первого ребёнка (через decrease-key, который вырезал ребёнка в корневой список). При потере второго ребёнка узел сам становится корнем - это и есть каскадный cut. Такая дисциплина гарантирует, что дерево не «разваливается» произвольно и сохраняет нижнюю оценку размера через числа Фибоначчи.
Операции и их амортизированная стоимость
insert(x): создать одноузловое дерево, добавить в root list, при необходимости обновитьmin. Амортизированно , реальное время - тоже .find-min: вернутьmin. - указатель поддерживается актуальным.merge(H1, H2): склеить два двусвязных кольца корней и взять минимум из двухmin-указателей. .extract-min: удалить корневой узел, всех его детей перенести в root list, затем выполнить консолидацию. Амортизированно .decrease-key(v, k'): уменьшить ключ. Если heap-order не нарушен - конец. Иначе вырезать из родителя в root list, снятьmarkу , и при необходимости запустить каскадный cut вверх. Амортизированно .delete(v):decrease-key(v, -\infty)+extract-min. amort.
Консолидация при extract-min
После удаления min его дети попадают в root list. Затем алгоритм заводит массив A размера примерно , индексированный по степени узла, и идёт по корневому списку. Для каждого корня степени : если A[d] пуст - кладём туда; если занят - сливаем с A[d] (меньший ключ становится корнем), степень итога , и пробуем уложить уже его (рекурсивно). В конце по A строится новый root list и обновляется min.
Время фактической работы - , но поскольку корней до консолидации могло быть много (после ленивых insert'ов), реальная стоимость может быть линейна. Амортизация делает оценку - за счёт того, что предыдущие insert-ы добавили потенциал.
Каскадный cut и правило mark
decrease-key имеет красивую дисциплину «mark-and-cut». Когда у узла вырезают одного ребёнка decrease-key'ем (а сам не корень), на ставится флаг mark. Если потом теряет ещё одного ребёнка - он сам вырезается в root list, его mark снимается, и проверка переходит на родителя . Так каскад поднимается по дереву, пока не встретит немаркированный узел или корень. На длинной серии cut'ов работа реальная немалая, но каждый cut уменьшает потенциал, и амортизированный счёт остаётся на одну decrease-key.
Правило гарантирует, что узел теряет не более одного ребёнка между двумя моментами, когда сам становится ребёнком. Это и есть структурный инвариант, на котором строится оценка через .
Потенциальная функция и почему
Анализ амортизации использует функцию
где - число деревьев в root list, - число помеченных узлов. insert увеличивает на 1 (один новый корень), потому амортизированная цена . extract-min уменьшает на (конечное число деревьев ), компенсируя реальное время. decrease-key тратит на самого себя плюс по на каждый каскадный cut, но каждый cut снимает mark и уменьшает на - и amortized cost схлопывается до .
Граница выводится так: пусть - минимальный размер поддерева, корень которого имеет степень . Из инвариантов получаем , где - числа Фибоначчи. Поскольку (), имеем , откуда .
Применение: Дейкстра на разреженных графах
Алгоритм Дейкстры с бинарной кучей даёт , потому что decrease-key стоит и вызывается до раз. Замена кучи на Фибоначчиеву превращает оценку в
На плотных графах () это уже против - реальное ускорение. Та же асимптотика всплывает в алгоритме Прима для MST и в ряде задач сетевого потока (например, Galil-Tardos для min-cost flow). На разреженных графах разница меньше, и из-за больших констант на практике часто оставляют бинарную или -арную кучу - теоретическая оптимальность куч Фибоначчи редко окупается в коде на C++.
Частые ошибки
- Думают, что Фибоначчи-куча быстрее во всех случаях. На малых и без массовых
decrease-keyбинарная куча на массиве почти всегда выигрывает по константам и cache locality. - Забывают про каскадный cut. Без него амортизированная оценка
decrease-keyломается, и в худшем случае операция станет линейной. Mark - не косметика, а часть доказательства. - Путают и высоту дерева. - это верхняя оценка степени корня, а не высоты; высота может быть больше из-за длинных «цепочек» после серии cut'ов.
- Считают, что extract-min - worst-case. Нет, только amortized. Один extract-min после миллиона insert'ов реально пройдёт по миллиону корней - амортизация распределит стоимость на предыдущие операции.
- Игнорируют размер константы. В научных бенчмарках куча Фибоначчи проигрывает -арной и парной (pairing) куче почти всегда - она нужна как теоретический инструмент.
FAQ
Почему именно «Фибоначчи»?
Потому что минимальный размер поддерева с корнем степени ограничен снизу числом . Это даёт и обеспечивает amortized на extract-min.
В чём принципиальная разница с биномиальной кучей?
Биномиальная куча поддерживает строгие и платит за insert и merge сразу. Куча Фибоначчи откладывает работу до extract-min, разрешает временно несбалансированные деревья и компенсирует это каскадным cut с mark.
Когда стоит реализовывать куча Фибоначчи в продакшене? Почти никогда. Её используют там, где нужно гарантированное для Дейкстры на плотных графах в теоретических работах. На практике предпочитают парные или -арные кучи - у них схожая асимптотика с лучшими константами.
Коротко
Куча Фибоначчи - ленивая mergeable-куча: insert, decrease-key и merge за амортизированное , extract-min и delete за amortized. Грязный root list очищается только при extract-min (консолидация), а правило mark-and-cut с каскадным вырезанием держит структуру деревьев в рамках . На алгоритме Дейкстры это даёт против на бинарной куче - теоретическая победа, на практике перекрываемая константами.
Читайте также

Биномиальная куча: операции и слияние за O(log n)
Биномиальная куча: как устроен лес деревьев, зачем нужно слияние двух куч за O(log n) и чем она лучше бинарной. Разбираем операции insert, extract-min и merge на примерах.

Splay-дерево: самобалансирующееся BST через продвижение к корню
Splay-дерево Слейтора и Тарьяна: операция splay через zig, zig-zig и zig-zag, амортизированная сложность O(log n) через potential function и сравнение с AVL/Red-Black.

B-дерево: вставка ключа и разделение узла
Разбираем вставку в B-дерево по шагам: минимальная степень t, инвариант t-1..2t-1 ключей и разделение переполненного узла с подъёмом среднего ключа к родителю.