# 【資工考研】Fibonacci Heaps 資料結構重點複習摘要 Fibonacci heap 支援 mergeable heap (e.g. Binomial Heap)的各個操作 (`Make-Heap()`, `Insert(H, x)`, `Minimum(H)`, `Extract-Min(H)`, `Union(H1, H2)`) 除此之外 Fibonacci heap 最大的特點在於它有些 operations run in constant amortized time 註: `Make-Heap()` 指建構空 heap ## 時間複雜度比較 | Procedure | Binary heap <br>(worst case) | Fibonacci heap <br>(amortized) | |:--- |:---:|:---:| | `Make-Heap` | $\mathrm{\theta} (1)$ | $\mathrm{\theta} (1)$ | | `Insert` | $\mathrm{\theta} (\lg n)$ | $\mathrm{\theta} (1)$ | | `Minimum` | $\mathrm{\theta} (1)$ | $\mathrm{\theta} (1)$ | | `Extract-Min` | $\mathrm{\theta} (\lg n)$ | $\mathrm{\theta} (\lg n)$ | | `Union` | $\mathrm{\theta} (n)$ | $\mathrm{\theta} (1)$ | | `Decrease-Key` | $\mathrm{\theta} (\lg n)$ | $\mathrm{\theta} (1)$ | | `Delete` | $\mathrm{\theta} (\lg n)$ | $\mathrm{\theta} (\lg n)$ | 註: `DECREASE-KEY(H,x,k)` 指將 heap `H` 中的元素 `x` 之值換成不比原本大的值 `k` ,也就是把 `x->val` 變小成 `k` 我們很清楚,一般的 binary heap 在 merge (`Union`) 時的表現可能會不太理想 Fibonacci heap 明顯的優勢就是均攤之下有些操作的表現比較好 可以看到有不少都能達到常數時間 以理論來說,我們這邊談論的場景主要是 `Extract-Min` 跟 `Delete` 兩個操作相對其他操作來得少次的情況,例如在圖論問題中有些演算法會需要對每個邊做 `Decrease-Key` 對於比較密集 (多邊) 的圖, Fibonacci heap 提供的常數時間操作會帶來很大的表現提升 然而回到現實面, Fibonacci heap 寫起來太麻煩了,沒事不會有人想用 (不是說完全沒人用) 考試這邊大概就考觀念,不過本文還是會大致講一下實作部分 ## 資料結構 Fibonacci heap 你可以想成是一個 Forest 的資料結構 它是一堆 heap-ordered trees ,也就是每棵樹都滿足父點不大於子點 (the key of a node is greater than or equal to the key of its parent) ![image](https://hackmd.io/_uploads/rysXMQt6C.png) > Each node `x` contains a pointer `x.p` to its parent and a pointer `x.child` to any one of its children. The children of `x` are linked together in a circular, doubly linked list, which we call the child list of `x`. Each child `y` in a child list has pointers `y.left` and `y.right` that point to `y`’s left and right siblings, respectively. If node `y` is an only child, then `y.left = y.right = y`. Siblings may appear in a child list in any order. 有了上述性質,我們只要找到 `root` 最小的樹,其 `root` 就是整個 Fibonacci heap 的最小值 用一個指標指向該節點,如此一來 `Minimum` 操作就能是 $\mathrm{\theta} (1)$ 兄弟之間會互指 (形成環狀雙向鏈結),子點都會指向父點,父點會指向自己的其中一個子點 如果是唯一的子點,就會左右指向自己 環狀雙向鏈結串列在這邊有兩個好處: 1. 我們可以在 $O(1)$ 時間內將節點插入到環狀雙向鏈結串列中的任何位置,或從其中的任何位置移除節點 2. 定兩個這樣的串列,我們可以在 $O(1)$ 時間內將它們拼接成一個環狀雙向鏈結串列 除此之外,節點的資料結構還會再包含兩個屬性: 1. `degree`: 節點的子串列節點數 2. `mark`: boolean 值,用來表示該節點是否失去了自己的子節點 Fibonacci heap 要刪或減時都挺麻煩的,上面這邊將在到時候派上用場 ```cpp! template <typename T> struct FibNode { T key; FibNode *left; FibNode *right; FibNode *parent; FibNode *child; int degree; bool mark; FibNode(T val) : key(val), left(this), right(this), parent(nullptr), child(nullptr), degree(0), mark(false) {} }; ``` 另外 Fibonacci heap 本身我們會再紀錄一下有幾個節點 ```cpp! FibNode<T> *minNode; int nodeCnt; ``` ## Potential Function For a give Fibonacci heap $H$, ew indicate by $t(H)$ the number of trees in the root list of $H$ and by $m(H)$ the number of marked nodes in $H$. We defined the potential $\phi (H)$ of $H$ by $$ \phi (H) = t(H) + 2m(H) $$ Let $D(n)$ be an upper bound on the maximum degree of any node in an $n$ nodes Fibonacci heap, $D(n)\le \left\lfloor \lg n \right\rfloor$ ## Union merge 2 Fibonacci heap 有兩類操作 這邊的 `Union` 精神即 lazy merge 基本就只是把 root 接起來而已,然後看一下現在 min 應該指向誰的 可想而知這邊會是 $O(1)$ ```cpp! // Fib-Heap-Union void merge(FibonacciHeap<T> &other) { lazyMerge(minNode, other.minNode); // 合併 root lists if (!minNode || (other.minNode && other.minNode->key < minNode->key)) minNode = other.minNode; nodeCnt += other.nodeCnt; other.minNode = nullptr; other.nodeCnt = 0; } ``` `minNode` 有兩種情況需要變成對方的 `minNode` : 1. 自己是空的 2. 對方有非空 `minNode` 且它才是最小 那之後就把節點數加起來 ## Insert 如果我們把要插入的節點看做一個 Fibonacci heap `Insert` 不就也是一種 `Union` 嗎? ```cpp! void insert(T key) { FibNode<T> *newNode = new FibNode<T>(key); lazyMerge(minNode, newNode); if (!minNode || newNode->key < minNode->key) minNode = newNode; ++nodeCnt; } ``` ## Minimum 給 `minNode` 就好,這邊很簡單 ## Extract-Min 前面都很簡單,這邊開始就真的複雜起來了 我們前面 merge 都用 lazy merge 應付著,但這邊開始我們就需要做 `consolidate` 了 要刪掉最小值的想法還是在 merge ,我們將 `minNode` 的每個子節點都當成 root ,並從 root list 中移除 `minNode` 這個過程你可想成原本的 Fibonacci heap 已經被搞散架了,此時我們需要將有相同度數的 tree 合併,直到不能再合併為止,這個視為一種修復重組,也就是 `consolidate` ```cpp! T extractMin() { if (!minNode) throw std::runtime_error("Fibonacci heap is empty!"); FibNode<T> *temp = minNode; if (temp->child) { FibNode<T> *child = temp->child; do { child->parent = nullptr; child = child->right; } while (child != temp->child); lazyMerge(minNode, temp->child); } temp->left->right = temp->right; temp->right->left = temp->left; if (temp == temp->right) minNode = nullptr; else { minNode = temp->right; consolidate(); } --nodeCnt; T res = temp->key; delete temp; return res; } ``` 上面這段 code 中 ```cpp! if (temp->child) { FibNode<T> *child = temp->child; do { child->parent = nullptr; child = child->right; } while (child != temp->child); lazyMerge(minNode, temp->child); } ``` 做的就是把 `minNode` 的子點都拆開來,使用 `lazyMerge` 把它們都先加到 root list ```cpp! temp->left->right = temp->right; temp->right->left = temp->left; ``` 之後,讓 `minNode` 的左右兄弟把手收回來 如果 `minNode` 的右邊指向自己,表示它是 root list 中唯一的那位且它沒有子點 除此之外我們就執行 `consolidate` ## Consolidate ```cpp! void consolidate() { if (!minNode) return; int maxDegree = static_cast<int>(std::log2(nodeCnt)) + 1; std::vector<FibNode<T> *> degreeList(maxDegree, nullptr); std::vector<FibNode<T> *> rootList; FibNode<T> *curr = minNode; do { rootList.emplace_back(curr); curr = curr->right; } while (curr != minNode); for (FibNode<T> *node : rootList) { int d = node->degree; while (degreeList[d]) { FibNode<T> *other = degreeList[d]; if (node->key > other->key) std::swap(node, other); link(other, node); degreeList[d++] = nullptr; } degreeList[d] = node; } minNode = nullptr; for (FibNode<T> *tree : rootList) { if (tree) { if (!minNode || tree->key < minNode->key) minNode = tree; } } } ``` 前面講過 $D(n)\le \left\lfloor \lg n \right\rfloor$ 所以 `maxDegree = static_cast<int>(std::log2(nodeCnt)) + 1` 那之後,假設我們在 root list 中找到兩個有相同 `degree` 的節點 `x` 跟 `y` ,不失一般性假設 $x\le y$ 那麼為了符合 minimum-heap property ,我們肯定是將 `y` 接給 `x` (使用 `link` 實現) 換句話說: `y` 會從 root list 中拔掉,而 `x` 的 `degree` 會增加 ```cpp! for (FibNode<T> *node : rootList) { int d = node->degree; while (degreeList[d]) { FibNode<T> *other = degreeList[d]; if (node->key > other->key) std::swap(node, other); link(other, node); degreeList[d++] = nullptr; } degreeList[d] = node; } ``` 最後那段是要重新選出 `minNode` 這邊不難看出時間複雜度會是 $O(D(n))$ (即使不實際算 amortized cost 大概也能看出) ## Decrease-Key ```cpp! void decreaseKey(FibNode<T> *node, T newKey) { if (newKey > node->key) throw std::runtime_error("New key is greater than current key"); node->key = newKey; FibNode<T> *p = node->parent; if (p && node->key < p->key) { cut(node, p); cascadingCut(p); } if (node->key < minNode->key) minNode = node; } ``` 我們把某個節點的值變小後,如果它比父點還小,那就破壞我們資料結構的性質了 ```cpp! // cut a node from its parent void cut(FibNode<T> *node, FibNode<T> *p) { if (p->child == node) p->child = (node->right == node) ? nullptr : node->right; node->right->left = node->left; node->left->right = node->right; --p->degree; lazyMerge(minNode, node); node->parent = nullptr; node->left = node->right = node; node->mark = false; } ``` 首先我們要先讓該節點與父點訣別 (使用 `cut`) ```cpp! // cascading cut to maintain properties void cascadingCut(FibNode<T> *node) { FibNode<T> *p = node->parent; if (p) { if (!node->mark) node->mark = true; else { cut(node, p); cascadingCut(p); } } } ``` 接著就是一直不斷往祖宗那挑戰 (`cascadingCut`) 可以看到在這邊我們才真正用到了 `mark` ,我們透過這個標記來記錄節點的歷史 當某個節點經歷了以下事件: 1. 當 root 2. 當別人子點 3. 失去**第二個子點**時,我們會把它 `cut` 掉 若`x` 經歷了1. 跟 2. 且已經失去一個子點,此時他的 `mark` 會被設成 `true` ,表示它離被 `cut` 掉只差一點點了 實際上就是第一次的 `cascadingCut` 會執行 `if (!node->mark)` 這段,但第二次再來就會執行到 `else` 了 這個動作可以預想,如果 `x` 是 `y` 失去的第二個子點,那 `y` 也要面對上述同樣的事情,對吧?所以這邊還要 `cascadingCut(p)` 另外容易忽略的是,在 `cut` 應該有注意到我們做了 `lazyMerge(minNode, node)` 此時 `node` 又再次經歷 1. ,所以我們要把 `mark` 重置回 `false` *** 假設原本有 $t(H)$ 顆 trees ,有 $c-1$ 顆 trees 經歷 `cascadingCut` 後被拆出來 假設總共有 `c-1` 顆的 `mark` 被 `cascadingCut` unmark ,但最後一次的 `cascadingCut` 可能會再 mark 一個 node ,故最多有 $m(H)-c+2$ 個 marked nodes 則 potential 的變化最多會是: $$ (t(H)+c-1+1 + 2(m(H)-c+2)) - (t(H)+2m(H)) = 4-c $$ 你會發現, `decreaseKey` 的 amortized cost 最多也就 $O(c)+4-c = O(1)$ Fibonacci 的一大考點就是它 `decreaseKey` 均攤之下僅需常數時間 ## Delete ```cpp! void removeNode(FibNode<T> *node) { decreaseKey(node, std::numeric_limits<T>::lowest()); extractMin(); } ``` 刪除節點就簡單了,我們只要將要刪除的節點值變成最小,`extractMin` 所刪掉的肯定是該點 ## 結尾 最後附上較完整的 code 我沒有認真 debug 過,大概看得懂概念就好,反正考試應該頂多考畫圖🤔 ```cpp! template <typename T> struct FibNode { T key; FibNode *left; FibNode *right; FibNode *parent; FibNode *child; int degree; bool mark; FibNode(T val) : key(val), left(this), right(this), parent(nullptr), child(nullptr), degree(0), mark(false) {} }; template <typename T> class FibonacciHeap { public: FibonacciHeap() : minNode(nullptr), nodeCnt(0) {} ~FibonacciHeap() { clear(minNode); } void insert(T key) { FibNode<T> *newNode = new FibNode<T>(key); lazyMerge(minNode, newNode); if (!minNode || newNode->key < minNode->key) minNode = newNode; ++nodeCnt; } // Fib-Heap-Union void merge(FibonacciHeap<T> &other) { lazyMerge(minNode, other.minNode); if (!minNode || (other.minNode && other.minNode->key < minNode->key)) minNode = other.minNode; nodeCnt += other.nodeCnt; other.minNode = nullptr; other.nodeCnt = 0; } T extractMin() { if (!minNode) throw std::runtime_error("Fibonacci heap is empty!"); FibNode<T> *temp = minNode; if (temp->child) { FibNode<T> *child = temp->child; do { child->parent = nullptr; child = child->right; } while (child != temp->child); lazyMerge(minNode, temp->child); } temp->left->right = temp->right; temp->right->left = temp->left; if (temp == temp->right) minNode = nullptr; else { minNode = temp->right; consolidate(); } --nodeCnt; T res = temp->key; delete temp; return res; } void decreaseKey(FibNode<T> *node, T newKey) { if (newKey > node->key) throw std::runtime_error("New key is greater than current key"); node->key = newKey; FibNode<T> *p = node->parent; if (p && node->key < p->key) { cut(node, p); cascadingCut(p); } if (node->key < minNode->key) minNode = node; } void removeNode(FibNode<T> *node) { decreaseKey(node, std::numeric_limits<T>::lowest()); extractMin(); } private: FibNode<T> *minNode; int nodeCnt; void lazyMerge(FibNode<T> *&h1, FibNode<T> *h2) { if (!h1) h1 = h2; else if (h2) { FibNode<T> *r1 = h1->right, *l2 = h2->left; h1->right = h2; h2->left = h1; r1->left = l2; l2->right = r1; } } void consolidate() { if (!minNode) return; int maxDegree = static_cast<int>(std::log2(nodeCnt)) + 1; std::vector<FibNode<T> *> degreeList(maxDegree, nullptr); std::vector<FibNode<T> *> rootList; FibNode<T> *curr = minNode; do { rootList.emplace_back(curr); curr = curr->right; } while (curr != minNode); for (FibNode<T> *node : rootList) { int d = node->degree; while (degreeList[d]) { FibNode<T> *other = degreeList[d]; if (node->key > other->key) std::swap(node, other); link(other, node); degreeList[d++] = nullptr; } degreeList[d] = node; } minNode = nullptr; for (FibNode<T> *tree : rootList) { if (tree) { if (!minNode || tree->key < minNode->key) minNode = tree; } } } void link(FibNode<T> *y, FibNode<T> *x) { y->left->right = y->right; y->right->left = y->left; y->parent = x; if (!x->child) { x->child = y; y->left = y; y->right = y; } else { FibNode<T> *xchild = x->child; y->right = xchild; y->left = xchild->left; xchild->left->right = y; xchild->left = y; } ++x->degree; y->mark = false; } // cut a node from its parent void cut(FibNode<T> *node, FibNode<T> *p) { if (p->child == node) p->child = (node->right == node) ? nullptr : node->right; node->right->left = node->left; node->left->right = node->right; --p->degree; lazyMerge(minNode, node); node->parent = nullptr; node->left = node->right = node; node->mark = false; } // cascading cut to maintain properties void cascadingCut(FibNode<T> *node) { FibNode<T> *p = node->parent; if (p) { if (!node->mark) node->mark = true; else { cut(node, p); cascadingCut(p); } } } void clear(FibNode<T> *node) { if (!node) return; FibNode<T> *start = node; do { FibNode<T> *child = node->child; if (child) clear(child); FibNode<T> *next = node->right; delete node; node = next; } while (node != start); } }; ```