本系列所有題目均為Acwing課的內容,發表博客既是為了學習總結,加深自己的印象,同時也是為了以后回過頭來看時,不會感嘆虛度光陰罷了,因此如果出現錯誤,歡迎大家能夠指出錯誤,我會認真改正的。同時也希望文章能夠讓你有所收獲,與君共勉!
昨天被并查集折磨了一天,今天終于可以放松點了。那么今天就主要來學習堆的操作。
這里的堆主要為大頂堆和小頂堆,他們都是以完全二叉樹作為數據結構的(完全二叉樹不清楚的可以自己去百度下),而完全二叉樹一般用數組模擬。接下來談談堆能干什么,我們知道每個父節點比子節點的值要大的堆是大頂堆,那么最大值也就是堆頂嘍,小頂堆同理,最小值也是堆頂,有了這樣的數據結構我們就能以\(O(1)\)的時間復雜度獲得最大值和最小值,而堆的主要操作分別是插入一個元素,刪除一個元素,修改一個元素,獲得集合中最小的元素。具體的就在下面講吧。
模擬堆
維護一個集合,初始時集合為空,支持如下幾種操作:
I x,插入一個數 x;
PM,輸出當前集合中的最小值;
DM,刪除當前集合中的最小值(數據保證此時的最小值唯一);
D k,刪除第 k 個插入的數;
C k x,修改第 k 個插入的數,將其變為 x;
現在要進行 N 次操作,對于所有第 2 個操作,輸出當前集合的最小值。
輸入格式
第一行包含整數 N。
接下來 N 行,每行包含一個操作指令,操作指令為 I x,PM,DM,D k 或 C k x 中的一種。
輸出格式
對于每個輸出指令 PM,輸出一個結果,表示當前集合中的最小值。
每個結果占一行。
數據范圍1≤N≤105
?109≤x≤109
數據保證合法。
輸入樣例:8
I -10
PM
I -10
D 1
C 2 8
I 6
PM
DM
輸出樣例:-10
6
算法原理
初始化
剛剛我們知道堆是用數組實現的完全二叉樹h[N]
,除此之外要想實現堆操作,還需要定義cnt
始終指向堆中最后一個結點(其實這時的cnt
就是這個元素在堆中的下標)和進來的順序m
,以及表示存儲x是第幾個進來的數組hp[N]
和存儲第m
個進來的數在堆h[N]
中所對應的下標ph[N]
。(這里m
和cnt
在數組都是從1開始,這樣初始化m=cnt=0
時就要先++m,++cnt
)。
差點忘了,建立一個堆就是不斷下沉元素的過程。
插入一個元素
在堆中我們插入一個元素是給h[cnt] = x
,同時需要記錄這個元素是第幾個進來的hp[cnt] = m
以及這時進來的元素在堆中存儲的下標也要被存儲起來ph[m] = cnt
,并且將這個元素上浮down(k)
(因為是存儲在堆最后的位置的)來尋找這個元素應該在堆中存儲的位置。
cin >> x;
++cnt,++m;
h[cnt] = x;
hp[cnt] = m,ph[m] = cnt;
up(cnt);
刪除任意一個元素
眾所周知,數組刪除可以用覆蓋來代替,那么如果我們需要刪除第k個數,我們通常會用最后一個數覆蓋他(方便),這里的覆蓋就是堆交換heap_swap(k,cnt)
,然后調整最后一個數到它應該到的位置,即上浮下沉一遍up(k).down(k)
,從樹的角度就是把最后一個數看看是往上走,還是往下走。
修改結點值
修改跟刪除一樣h[k] = x
,刪除完之后就調整修改后節點的位置,即上浮下沉一遍。
獲得集合里的最值
這個更簡單,下標為1的結點就是堆頂h[1]
,就是最大值或最小值,直接輸出就行。
這些操作中就有堆的核心操作,上浮和下沉,還有堆交換(比較難理解),接下來就來介紹這些核心操作,以小頂堆為例。
上?。▍凳窃卦诙阎械南聵耍?/h3>
由小頂堆的性質我們可以知道,如果一個節點比他的父節點小,那么我們就需要把他上浮,那么怎么上浮呢,我們可以一直比較該節點與其父節點的大小,如果比它小,就進行堆交換,那么什么時候停止呢?當該節點比父節點要大時就滿足小頂堆性質了,可以停止循環,還有一種情況就是該結點就是最小值,那么它就會到這個堆的堆頂去,即當前節點的下標為1時也要停止循環。
void up(int x){
while(x/2 || h[x/2] > h[x]){
heap_swap(x,x/2);
x >>= 1;
}
}
下沉(參數是元素在堆中的下標)
同理,如果一個結點值比他的子節點要大,他就要下沉,由于子節點有兩個-左右兒子,因此需要比較與子節點的大小,如果滿足條件就先堆交換在下沉,不斷下沉直至到達合適的位置,當然要是不滿足條件就不必下沉。
void down(int x){
int t = x;
if(2*x <= cnt && h[2*x] < h[t]) t=x*2;
if(2*x+1 <= cnt && h[2*x+1] < h[x]) t=2*x+1;
if(x!=t){
heap_swap(x,t);
down(t);
}
}
堆交換(參數是元素在堆中的下標)
對于x,y兩個數,首先要知道他們當前在堆中的下標a
,b
然后才能heap_swap(a,b)
,那么怎么交換這兩個數在堆中的位置呢。
首先就得要知道他們什么時候進來的hp[a]
,hp[b]
,交換這個順序在堆中存儲的下標swap(ph[hp[a]],ph[hp[b]])
。
然后交換他們進來的先后順序swap(hp[a],hp[b])
。
最后在交換在堆中的值swap(h[a],h[b])
。
void heap_swap(int a,int b){
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
代碼實現
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000010;
int h[N]; // 存儲堆的值
int hp[N]; // 下標為a所對應的數--表示一種順序
int ph[N]; // 第k個數所對應的下標a--指針
int cnt,m; // 最后一個數
// 交換三組:
// 1.對這兩個數的下標所對應的順序值,第幾個值(順序值)在堆中所對應的下標進行交換
// 2.先交換存儲在ph中某兩個相對(插入的順序)的數所指向堆中的下標
// 3.最后交換a,b這兩個位置的值
void heap_swap(int a,int b){
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a],hp[b]);
swap(h[a],h[b]);
}
void down(int x){
int t = x;
if(x * 2 <= cnt && h[2*x] < h[t]) t = 2*x;
if(x * 2 + 1 <= cnt && h[2*x + 1] < h[t]) t = 2*x+1;
if(x != t){
heap_swap(x,t); // 交換x,t,與堆排序的不同,這里使用的是heap_swap,只需要提供下標,再函數內部堆下標所指向的數及進行排序
down(t);
}
}
void up(int x){
while(x / 2 && h[x/2] > h[x]){ // x/2
heap_swap(x,x/2);
x >>= 1;
}
}
int main()
{
int n;
cin >> n;
while(n--){
string op;
int x,k;
cin >> op;
if(op == "I"){
cin >> x;
++cnt,++m;
h[cnt] = x; // cnt所對應存儲在堆h中的值
ph[m] = cnt,hp[cnt] = m; // cnt是下標,m是第幾個數,都從1開始
up(cnt); // 插入位置的元素上浮
}
else if(op == "D"){ // 刪除:刪除第k個數,那就拿最后一個數覆蓋他,然后對這個數調整
cin >> k;
k = ph[k];
heap_swap(k,cnt);
cnt--;
up(k); // 不管交換后最后一個的值是大還是小,都先上浮或下浮一遍.
down(k);
}
else if(op == "C"){
cin >> k >> x;
k = ph[k]; // 找到第k數所對應的下標
h[k] = x; // 將對應的位置修改值
up(k); // 調整修改后的數的位置
down(k);
}
else if(op == "PM"){
cout << h[1] << endl;
continue;
}
else{ // "DM"刪除最小值
heap_swap(1,cnt);
cnt--;
down(1); // 只能向下調整
}
}
return 0;
}