明兒就是2017NOIP初賽了,老師還說上午依舊進行模擬賽,下午參加初賽,然而迷迷糊糊的我此時在被窩里寫起了PairingHeap的學習小結,老師對我的不滿度可能又上升了(如果他知道的話)。
[產品特色]
①沛堆堆(亂取的綽號)是一顆多叉樹。
②包含Priority_Queue的所有功能,可用於優化最短路。
③屬於可並堆,因此對於集合合並維護最值的問題很實用。
④速度快於一般的堆結構(左偏樹,斜堆,隨機堆……),具體快在這里:
這里就順帶引出它的基本操作啦:
·合並(Merge): O(1)
·插入(Insert/Push): O(1)
·修改值(Change): O(1)/O(logn)
·取出維護的最值(Top): O(1)
·彈出堆頂元素(Pop): O(logn)
[功能介紹]
①可並堆的靈魂——Merge操作
這里令人驚奇的是,配對堆只需要O(1)的時間完成這一步,具體做法為比較兩個需要合並的堆的根的權值大小,然后就將那優先級較低(比如你要求大的在堆頂,那么權值越大,優先級越高)置為另一個點的兒子,即fa[v]=u,再將u向v建邊即可。
②經典操作之一——插入(Push/Insert)操作
就很容易了,將插入元素新建為一個單獨的節點作為一個堆,與當前的堆進行Merge操作就可以了。
③經典操作之二——取最值(Top)操作
就直接用Root記錄堆根,然后返回val[Root]就美妙完成任務。
④重要而具有特色的操作——修改操作(Change)
修改一個節點的的權值,那么怎么處理來繼續保持配對堆的堆性質?首先將這個點和父節點的連邊斷掉,即fa[u]=0(由於父節點連邊使用鏈式前向星,不方便刪除,就不刪除,但是這樣並不會影響正確性,因為后文枚舉一個點的兒子節點時,要確認某個點是它的兒子節點,不僅是要這個點能夠有邊指向這個兒子,同時需要這個兒子的fa[]中存儲的就是這個節點)。
斷掉與父親的連邊后,相當於形成兩個堆,接下來進行一次Merge操作就好了。可以發現這個操作的時間復雜度是O(1),但有資料認為這個操作可能會破壞配對堆應有的結構(這"應有"的結構在下文會體現出來,它是Pop操作是O(logn)而不是O(n)的重要保證),結構改變后就會影響Pop的復雜度,使其向 O(n)退化,因此計算后認定其實修改操作從時間復雜度貢獻分析來看,可能是O(logn)而不是O(1)。
⑤最緩慢但很重要的操作——彈出最值(Pop)操作
你會發現上文的操作都那么偷懶,幾乎都是胡亂Merge一下,Merge函數又是隨隨便便連一條邊就完事兒了……因此這個操作需要來收拾這個爛攤子。我們現在的任務是刪除根節點,那么我們就要從它的兒子中選出合法繼承人。如果直接將所有兒子挨個挨個Merge起來,那么這樣很容易使得一個點有很多個兒子,從而影響后來的Pop操作時間,將O(logn)退化為O(n)。較快的做法是將子樹兩兩合並,不斷這樣合並,最終形成一棵樹,同理,這樣之所以快是因為保證了后面pop操作時候點的兒子個數不會太多。
[要點嘗鮮]
①鏈式前向星建邊:
②Merge操作:
③Insert/Push操作:
④ChangeVal操作:
⑤Top操作:
⑥Pop操作:
[產品代碼]
接下來一份簡潔的代碼,內容是將n個數排序。
其中的Stack是用來回收空間的。這里沒有給出ChangVal函數,原因是這個函數適用於有特定位置的元素的修改,比如將數組插入堆,然后修改數組下表為i的元素權值。上文內容毫無保留地講述了ChangVal的內容,直接打就是了。
同樣的,如果要用來維護一些信息,比如Dijkstra的優化,那就在點的信息上添加記錄最短路中點的編號之類的形成映射以達成快速取值的目的,其實呢和STL優先隊列是一樣的。
1 #include<stdio.h> 2 #define go(i,a,b) for(int i=a;i<=b;i++) 3 #define fo(i,a,x) for(int i=a[x],v=e[i].v;i;i=e[i].next,v=e[i].v) 4 const int N=10000010; 5 int n,a[N],b[N]; 6 7 struct Stack 8 { 9 int S[N],s=0,k=0; 10 int get(){return s?S[s--]:++k;} 11 void Save(int index){S[++s]=index;} 12 }Node,Edge; 13 14 struct Pairing_Heap 15 { 16 int sz=0,fa[N],head[N],k,val[N],Root; 17 int S[N],s;struct E{int v,next;}e[N]; 18 19 void ADD(int u,int v){e[k=Edge.get()]=(E){v,head[u]};head[u]=k;} 20 int Merge(int u,int v){val[u]>val[v]?u^=v^=u^=v:1;ADD(fa[v]=u,v);return u;} 21 void Push(int Val){int u=Node.get();val[u]=Val;Root=Root?Merge(Root,u):u;} 22 int Top(){return val[Root];} 23 24 void Pop() 25 { 26 s=0;fo(i,head,Root)Edge.Save(i),fa[v]==Root?fa[S[++s]=v]=0:1; 27 fa[Root]=head[Root]=0;Node.Save(Root);Root=0; 28 int p=0;while(p<s){++p;if(p==s){Root=S[p];return;} 29 int u=S[p],v=S[++p];S[++s]=Merge(u,v);} 30 } 31 }q; 32 int main() 33 { 34 scanf("%d",&n); 35 go(i,1,n)scanf("%d",a+i),q.Push(a[i]); 36 go(i,1,n)printf("%d\n",q.Top()),q.Pop();return 0; 37 }//Paul_Guderian
大米飄香的總結:
沛堆堆繼承了斐波拉契堆的優秀操作復雜度,同時相比之下降低了空間復雜度和代碼復雜度,這樣優美高效的數據結構當然適合用在競賽領域。如果談到什么時候會用到沛堆堆,大米餅認為主要是兩個方面——代替優先隊列和代替常規的可並堆。常規可並堆如斜堆,隨機堆和左偏樹雖然代碼更短,但是時間復雜度不夠理想,再說了沛堆堆代碼其實也很短的(這使得我們可以直接手寫而不用冒風險去調用STL中的沛堆堆了)。最后這篇博文有一個小小的缺陷是,由於大米餅笨笨的,其實上文中ChangeVal是有局限的,其中修改值只能比原值小(如果越小優先級越高的話),因為如果修改為較大值,其操作就類似與Pop了,雖然個人認為時間復雜度由於都是O(logn)不會影響,但畢竟還沒試驗過,須謹慎使用。如果大米餅出錯了或者出現冗余,希望來瀏覽的人加以指出批評。
我相信這生命總有輝煌,夢想總會在不遠的地方,
朋友請陪着我走過坎坷,當我眼淚流淌就在這沉默不羈的大橋上。————汪峰《大橋上》