Chapter III. 漸入佳境
基礎資料結構
Heap
作者8e7、建中大講義團隊
先備知識二元樹

堆積(Heap)


堆積(Heap) 是一個相對簡單的樹狀資料結構。他的實作概念是基於二元樹,而樹上的每一個節點儲存一個可以比較大小的鍵值(key),並支援以下操作:

堆積會根據根節點的值是最小或最大值分為最小堆(Min-Heap)和最大堆(Max-Heap)。對於一個 Min-Heap,一個節點的值一定比其子樹的每一個節點的值都小,對於一個 Max-Heap,則是都大。還有一個重要的性質就是:通常實作的時候會把這個樹當成一個完全二元樹(也就是除了最後一階可能不填滿其他都滿)的樹,這個好處就是可以用陣列存這個樹。

Tips 1
用陣列存二元樹

給定一個陣列,可以用陣列來存二元樹,節點由上到下,由左到右一層層編號:

經觀察可以發現:編號為 $i$ 的節點左節點為 $2i$,右節點為 $2i + 1$,而其父節點為 $\lfloor \frac{i}{2} \rfloor$。如果是完全二元樹,就可以發現只需要 $O(n)$ 的空間來存;但是如果退化成鏈可就糟糕了:需要 $O(2^n)$ 的空間存,所以這種儲存方法最適合用在高度不高(像是 $O(\log n)$)的樹,而 Heap 正好符合這個性質。

顯然,對於一個數字的集合,也會有許多的 Min(Max) Heap 可能。以下所講的 Heap 皆指 Min-Heap,至於 Max-Heap 的實作就只是把不等號反過來就好了。另外,以下所說的「最後的節點」指的是最後一層最右邊的節點。

要支援的操作比較多,讓我們一個個拿出來討論。這邊我們用 vector 做為一種陣列的實作。

不難發現,insert()deleteRoot() 的複雜度都是 $O(\log n)$,因為最差就是一直遞迴到底,而因為是完全二元樹,就有 $O(\log n)$ 層。

STL 內的 Heap


當然,超棒的 STL 有提供 Heap 可以用,稱為 priority_queue,但是不一定是依照以上的實作。

只要符合「一個節點的值一定比其子樹的每一個節點的值都小(大)」的性質的樹都可以是 Heap。關於其他種 Heap 的實作可以參考這裡,而我們實作的 Heap 則是被稱作「Binary Heap」。

宣告

我們可以用下列方法宣告一個 priority_queue

priority_queue<int> pq; // 宣告一個整數的 max-heap
 

不過就好比排序,我們有時候會想要自定義排序基準,在 priority_queue 我們也會想要定義誰是「最大值」。若要自行定義比較函數(也就是定義「小於」),可以下列方式進行:

priority_queue<int, vector<int>, greater<int> > min_pq;
// 把「小於」定義成「大於」,也就是 min-heap
priority_queue<int, vector<int>, comp> custom_pq; 
// 自訂義小於比較

priority_queue 的比較函數稍微複雜一點,要用 structoperator() 來實作,以下是實作方法:

struct comp {
    bool operator()(int a,int b) {
        return a % 10 < b % 10;
    }
};

上述 comp 放進 priority_queue 之後,製造出來的效果會是維護「數字的末位數最大的值」。

priority_queue 的比較函數是定義「小於」,但實際上他是維護「最大值」,這可能跟 sort 的邏輯有些不同,需要特別注意。

STL Heap 操作

STL 內建的 priority_queue 一樣支援了 heap 該有的操作:

pq.push(3);
pq.push(2);
cout << pq.top() << endl; // 3
pq.pop();
cout << pq.top() << endl; // 2

千萬不要對一個空的 priority_queue 呼叫 top() 函數,不然你會吃 RE。

習題


Heap 可以運用在非常多的題目內,不過那是未來篇章的事情了。這裡就先放上一些基本的操作題:

習題
排隊買飲料
Source:TIOJ 1999

有一天,你經過了一家飲料店,發現有 $N$ 個人排隊要買飲料。不過,這家飲料店人手充足,一共有 $M$ 個店員可以服務這些排隊的人潮。
為了公平起見,雖然店員很多,但是排隊只排成一列,避免在排很多列的情況下,每列前進的速度會不一樣。

另外,為了服務品質起見,這家店的店員必須遵守兩個規則:必須按照客人排隊順序服務客人,不能先服務排在後面的顧客,而且,如果某個店員服務了某個客人,則該客人點的所有飲料都要由該店員製作,製作完成之後才能服務下一位客人。

你做了一下市場調查,詢問每位排隊的客人要買幾杯飲料。假如所有的店員製作飲料的速度都是每分鐘 $1$ 杯,在遵守這些規則的前提下,請問服務完這些客人至少需要多久?

條件限制
  • $N \leq 10^6$
  • $M \leq 10^4$
  • 每個排隊顧客最多買 $1000$ 杯飲料
習題
進階的入門
Source:TIOJ 1168

請支援五種操作:

  • 將某個最大值移除
  • 將某個最小值移除
  • 加入一個數
  • 詢問當前最大值
  • 詢問當前最小值
條件限制
  • 操作數量 $\leq 10^6$
  • 時限限縮至 300ms