Фібоначчієва купа — Вікіпедія

Операція Амортизаційний час
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, то

Операції

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

Вставка дужа проста: додаємо новий елемент 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 Фібоначчієві купи. Вступ до алгоритмів (вид. 3rd). MIT Press і McGraw-Hill. ISBN 0-262-03384-4.
  • Fibonacci Heaps на www.cs.princeton.edu