Фібоначчієва купа

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку
Операція Амортизаційний час
Make-Heap
Insert
Minimum
Extract-Min
Union
Decrease-Key
Delete

Купа Фібоначчі — структура даних, ефективна реалізація черги з пріоритетом.

З теоретичної точки зору купи Фібоначчі особливо варто використовувати, коли кількість Extract-Min і Delete операцій мала порівняно з кількістю інших операцій. Наприклад, деякі алгоритми на графах можуть викликати Decrease-Key на кожному ребрі. Для щільного графу сталий амортизаційний час кожного виклику Decrease-Key складається у велику перевагу в порівнянні з логарифмічним часом у найгіршому випадку в бінарній купі. Це можна побачити на прикладі алгоритмів про найкоротші шляхи з одного входу і мінімального кістякового дерева. З практичної точки зору сталий множник прихований у складності алгоритму і складність у програмуванні купи Фібоначчі роблять її менш бажаною, ніж звичайну бінарну або d-арну купу для більшості застосувань.

Історія[ред. | ред. код]

Фібоначчієву купу запропонували Фредман і Тар'ян у 1984 році. Головним гаслом цієї конструкції було «ми виконуємо роботу лише коли мусимо, і тоді ми використовуємо це для спрощення структури настільки наскільки можливо, таким чином, щоб майбутня робота була легкою».

Структура купи[ред. | ред. код]

Зображення 1. Приклад Фібоначчієвої купи. Купа має три дерева степенів 0, 1 і 3. Три позначені вершини (синім). Отже, потенціал купи становить 9 (3 дерева + 2 × (3 позначених-вершини)).

Купа Фібоначчі являє собою набір дерев, кожне з яких є мінкупою змінної арності з такими властивостями:

  1. Вузли дерев відповідають елементам, що зберігаються в черзі.
  2. Корені куповпорядкованих дерев поєднані у двобічно зв'язаний список.
  3. Зберігається вказівник на корінь дерева, який відповідає елементу з найменшим ключем (варто зауважити, що те, що кожне дерево є мінкупою, гарантує, що цей елемент буде коренем одного з дерев).
  4. Для кожного вузла зберігається його ранг (степінь), тобто кількість його дітей, а також чи він позначений (мету позначення ми визначимо пізніше).
  5. Вимога розміру: якщо вузол u має степінь k, то піддерево з коренем u має щонайменше вузлів, де  — це i-те число Фібоначчі, тобто і для

Кожен вузол x містить вказівник x.p на свого предка і вказівник x.child на один із його нащадків. Нащадки зв'язані в циклічний двобічно зв'язаний список. Кожен нащадок y має вказівники y.left та y.right. Якщо вузол є єдиним нащадком, тоді y = y.left = y.right. Нащадки елемента можуть перебувати у будь-якому порядку. Використання двобічно зв'язаних списків уможливлює вставляння і видалення елементів за час O(1), а також зв'язування двох списків за O(1). Також кожен вузол має атрибут x.degree, що дорівнює кількості нащадків, і атрибут x.mark, що показує, чи втратив вузол нащадка з моменту, як він став нащадком свого поточного предка.

Доступ до купи відбувається через вказівник H.min, який вказує на корінь дерева з найменшим ключем. Якщо дерево порожнє, то H.min дорівнює NIL.

Для оцінки швидкості використовують метод потенціалів із потенціальною функцією Де дає кількість дерев у купі, а  — кількість позначених вузів.

Максимальний степінь

Надалі вважатимемо, що нам відома верхня межа D(n) максимального степеня вузла у Фібоначчієвій купі з n вузлами. Якщо купа підтримує лише операції поєднуваної купи, тоді Хоча, навіть якщо Фібоначчієва купа підтримує Decrease-Key та Delete, то

Операції[ред. | ред. код]

INSERT[ред. | ред. код]

Вставка дужа проста: додаємо новий елемент s як нову мінкупу в колекцію і перевіряємо, чи k(s) менше за поточне найменше значення. Якщо так, то відповідно змінюємо вказівник H.min.

Insert(H, x)
 1 x.degree = 0
 2 x.p = NIL 
 3 x.child = NIL
 4 x.mark = FALSE
 5 якщо H.min == NIL
 6     створити список коренів для H, що містить лише x
 7     H.min = x
 8 інакше вставити x у список коренів H
 9     якщо x.key < H.min.key
10         H.min = x
11 H.n = H.n + 1

Щоб визначити амортизаційну вартість вставляння елемента, нехай H буде початковою Фібоначчієвою купою і H' буде результовною. Тоді і отже збільшення потенціалу становить

Оскільки амортизаційна вартість дорівнює сумі дійсної вартості і різниці потенціалів, то вона дорівнює

DECREASE-KEY[ред. | ред. код]

Фібоначчієва купа із зображення 1 після зменшення ключа з 9 до 0. Цей вузол, як і два його позначені попередники, відтинаються від дерева з коренем 1 і розташовуються як нові корені.

Коли ми зменшуємо ключ елемента s, якщо умови мінкупи все ще задовільняються, то нам більше не потрібно робити нічого. Інакше, ми просто відтинаємо піддерево з коренем в s і вставляємо його як нове піддерево у колекцію. Ми порівнюємо новий ключ s із попереднім мінімальним елементом і змінюємо вказівник H.min відповідно.

Таким чином, ми отримуємо щось, що виглядає подібно до бажаної Фібоначчієвої купи. Однак, проблема із простим відтинанням кожного такого s полягає в тому, що ми можемо врешті решт порушити вимогу розміру. Отже, щоб полегшити ситуацію, ми впроваджуємо додаткове правило, що коли ми відтинаємо s ми перевіряємо, чи не помічений його батьківський елемент. Якщо так, то ми також відтинаємо батьківській елемент і знімаємо позначку з нього. Інакше, ми просто позначаємо батьківський елемент. Перевірка батьківського елемента виконується рекурсивно, тобто, якщо батько батька позначений, ми відтинаємо і його також. Очевидно, що якщо ми відтинаємо корінь, то ми не робимо нічого, тому немає сенсу позначати корінь. Тому, ця потенційно каскадна процедура, завжди завершується.

Decrease-Key(H,x,k)
1 якщо k > x.key
2     помилка "новий ключ більший ніж попередній"
3 x.key = k
4 y = x.p
5 якщо y != NIL і x.key < y.key
6     Cut(H,x,y)
7     Cascading-Cut(H,y)
8 якщо x.key < H.min.key
9     H.min = x
Cut(H,x,y)
1 видалити x зі списку дітей y, зменшити y.degree
2 додати до списку коренів H
3 x.p = NIL
4 x.mark = FALSE
Cascading-Cut(H,y)
1 z = y.p
2 якщо z != NIL
3     якщо y.mark == FALSE
4         y.mark = TRUE
5     інакше Cut(H,y,z)
6            Cascading-Cut(H,z)

З'ясуємо дійсну вартість зменшення ключа. Decrease-Key потребує O(1), не враховуючи каскадного видалення. Припустимо, що відбулось c викликів Cascading-Cut. Виконання кожного Cascading-Cut, не враховуючи рекурсивних викликів, потребує O(1). Отже дійсна вартість Decrease-Key становить O(c).

Тепер обчислимо різницю потенціалів. У висліді зменшення ключа утворюється c-1 нових дерев через каскадні відтинання і дерево з коренем x. c-1 вузів припиняють бути позначеними. І, можливо, позначається один вузол.

З цього випливає, що амортизаційна вартість буде не більше ніж

EXTRACT-MIN[ред. | ред. код]

Фібоначчієва купа із зображення 1 після першої фази вилучення мінімального елемента. Вузол з ключем 1 (мінімум) було видалено і його діти були додані як окремі дерева.
Фібоначчієва купа із зображення 1 після виконання вилучення найменшого елемента. Спершу, поєднані вузли 3 і 6. Потім результат поєднаний із деревом з коренем у вузлі 2. Насамкінець, знайдено новий мінімум.

Насамкінець, ми можемо описати видобування мінімального елемента s*. Починаємо з видалення s* (на нього вказує H.min) і додавання усіх його дітей як дерев у колекцію. Тепер, ми продивляємось усю колекцію, знаходимо найменший елемент і оновлюємо H.min відповідно.

На цьому можна було б і завершити, оскільки ми отримали правильну Фібоначчієву купу. Однак, не складно побачити, що досі описані операції над купою, роблять список коренів довшим і довшим. Отже, проходження через увесь список коренів ставатиме обчислювально дорожчим. Отже, якщо ми вже маємо зробити якусь роботу у будь-якому випадку, то ми виконаємо очищення зараз, щоб уникнути більшої роботи у наступному. Тому, доти доки наявні два дерева з однаковим рангом, скажімо k, ми зливаємо ці дерева, щоб отримати дерево рангу k+1. Злиття полягає у порівнянні коренів і додавання дерева з більшим коренем як дочірнього до другого кореня. Зауважимо, що оскільки злиття може створити друге дерево рангу k+1 у колекції, один корінь може взяти участь у кількох злиттях.

Extract-Min(H)
 1 z = H.min
 2 якщо z != NIL
 3     для кожної дитини x z
 4         додати x до списку коренів H
 5         x.p = NIL
 6     видалити z зі списку коренів H
 7     якщо z == z.right // Вважаємо, що видалення z із двобічного списку не змінює її right/left вказівників
 8         H.min = NIL
 9     інакше H.min = z.right
10         Consolidate(H)
11     H.n = H.n - 1
12 повернути z

Після видалення z ми консолідуємо список коренів H. Консолідація відбувається виконуючи наступні кроки допоки у списку коренів присутні корені з однаковим степенем.

  1. Знайти два корені x, y з однаковим степенем. Припустимо, без втрати загальності, що x.key =< y.key.
  2. Приєднуємо y до x. Тут ми збільшуємо x.degree і очищаємо позначку на y.
Consolidate(H)
 1 нехай A[0..D(H.n)] буде новим масивом
 2 для i = 0 до D(H.n)
 3     A[i] = NIL
 4 для кожного вузла w у списку коренів H
 5     x = w
 6     d = x.degree
 7     поки A[d] != NIL
 8         y = A[d] // Інший корінь з тим самим степенем як і x
 9         якщо x.key > y.key
10             обміняти x і y
11         Heap-Link(H,y,x)
12         A[j] = NIL
13         d = d + 1
14     A[d] = x
15 H.min = NIL
16 для i = 0 до D(H.n)
17     якщо A[i] != NIL
18         якщо H.min == NIL
19             створити список коренів для H, що містить лише A[i]
20             H.min = A[i]
21         інакше вставити A[i] у список коренів H
22             якщо A[i].key < H.min.key
23                H.min = A[i]
Heap-Link(H,y,x)
1 видалити y зі списку коренів H
2 зробити y дочірнім для x, збільшити x.degree
3 y.mark = FALSE

Обчислимо дійсну вартість видобування найменшого елемента. O(D(n)) маємо завдяки Extract-Min, через необхідність обробити усі дочірні вузли, і завдяки циклам 2-3 і 16-23 у Consolidate. Розглянемо внесок циклу 4-14 Consolidate, для цього використаємо груповий аналіз. Розмір списку коренів перед викликом Consolidate не більше ніж D(n) + t(H) + 1. Ми знаємо, що кожен раз у тілі циклу while один з коренів приєднується до іншого, отже, загальна кількість ітерацій циклу while у всіх ітераціях зовнішнього циклу for не може перевищити розмір списку коренів. Тому загальна кількість роботи у циклі 4-14 щонайбільше пропорційна до D(n) + t(H). Отже, всього нам треба O(D(n) + t(H)).

Потенціал перед видобуванням мінімального вузла є t(H) + 2m(H), а після — не більше ніж D(n) + 1. З цього і дійсної вартості маємо амортизаційну вартість

Обмеження на найбільший степінь[ред. | ред. код]

Лема. Нехай  це вузол, і нехай  позначають його дітей у порядку в якому вони були додані до  Тоді:

Доведення: 
Очевидно, що y1.degree ≥ 0. Для i ≥ 2, коли ми yi додавали до x, там вже було щонайменше i-1 дочірніх елементів, значить на момент додавання степінь yi мав дорівнювати степеню x. З того часу один з дочірніх елементів y міг бути відрізаним. Отже, степінь yi міг зменшитись на 1 і стати i-2.
Факти про числа Фібоначчі: 
1. 
2.  де 
Лема. Нехай x буде вузлом у купі Фібоначчі і нехай k = x.degree. Тоді size(x) ≥ Fk+2 ≥ φk.
Наслідок: Найбільший степінь D(n) будь-якого вузла в n-вузловій Фібоначчієвій купі є O(lg n).
Доведення: Нехай x буде довільним вузлом і k=x.degree. Маємо, що n ≥ size(x) ≥ φk. Логарифмуючи з основою φ отримуємо k ≤ logφ n. Оскільки k ціле, то 

Джерела[ред. | ред. код]

  • Томас Кормен; Чарльз Лейзерсон, Рональд Рівест, Кліфорд Стайн (2009) [1990]. 19 Fibonacci Heaps. Вступ до алгоритмів (вид. 3rd). MIT Press і McGraw-Hill. с. 505—530. ISBN 0-262-03384-4.
  • Fibonacci Heaps на www.cs.princeton.edu