1. 程式人生 > 實用技巧 >資料結構-優先佇列與堆

資料結構-優先佇列與堆

前言

​ 好吧,我把大部分圖片都改成上傳相簿用地址打開了,這樣128M應該還能讓我再撐一會,回頭如果有錢再買個雲伺服器吧= =。那麼今天就接著來寫另一個數據結構--堆qwq

優先佇列

​ 優先佇列指的是支援每次挑選出優先順序最高的元素出隊,同時支援元素入隊的佇列。而這兩個操作的實現我們往往使用的是二叉堆。

二叉堆

​ 二叉堆(後面簡稱堆)是一個完全二叉樹(注意與滿二叉樹區分開),且滿足父節點的值不小於子節點的值。由於是完全二叉樹,堆的每個節點可以直接找到自己的父節點(pos>>1)和左右子節點(pos <<1和(pos <<1 )+ 1),且最底層的葉節點儘量靠左。二叉堆常用操作就是insert插入元素和deleteMin刪除最小值

程式碼測試題目為洛谷模板題,完整程式碼會另外出部落格

insert

​ 現在要將一個元素插入到堆中那麼直接將它放到最後一個然後和父節點比較,若新元素優先順序大(優先順序取決於自己定義)則與父節點互換,直至新元素的位置滿足要求。

程式碼實現:

void insert(int x) {
    hep[++tot]=x; int now=tot; //hep[]儲存整個堆 tot為堆當前總元素量
    while((now>>1) && hep[now>>1]>hep[now])
         swap(hep[now>>1],hep[now]),now>>=1;
}

deleteMin

​ 刪除最小隻需要將hep[1]與hep[tot]交換然後tot--,之後讓根不斷向下沉(具體來說就是和較小的子節點交換)直至滿足堆的形式。

程式碼實現:

void deleteMin() {
    hep[1]=hep[tot]; tot--;
    for(int now=1,minn;(now<<1)<=tot;now=minn) {
        int ls=now<<1,rs=(now<<1)+1;
        if(hep[ls]<hep[rs]) minn=ls; else minn=rs;
        if(hep[now]>hep[minn]) swap(hep[now],hep[minn]);
    }
}

可並堆

​ 可並堆也是一個抽象資料結構,就是在原來優先佇列的基礎上增加一個合併merge操作,即將兩個可並堆合併為一個。下面說明幾種實現可並堆的方法。

左偏樹

定義

​ 左偏樹的節點在有一個鍵值的基礎上增加了一個距離(dis)值,表示當前節點到它後代中最近的外節點中間的邊數,其中外節點指的是左子樹或右子樹為空的節點。特別的,外節點的dis值為0,空節點的dis值為1,左偏樹的距離指的是根節點的dis值。

​ 左偏樹是一個二叉樹,結構在堆序(滿足父節點不大(小)於子節點,但不保證是完全二叉樹)的基礎上還要求左子節點的dis值不小於右子節點(即左偏性質)。

性質

  1. 跟據左偏性質,我們可以發現父節點的dis值為右子節點的dis+1。
  2. 若左偏樹的距離為定值,節點數最少的左偏樹為完全二叉樹。節點數最少即當dis[ls[i]]==dis[rs[i]]時,形成結構就是滿二叉樹。
  3. 若左偏樹距離為k,節點數最少為 \(2^{k+1}-1\) 。由2節點數最少時是一個高為k的滿二叉樹,滿二叉樹節點數\(2^{k+1}-1\)
  4. 節點為n的左偏樹距離最大為 $ \left\lfloor\log(n+1)\right\rfloor-1$ 。其實就是由3推出。

基本操作

程式碼測試題目為洛谷模板題 ,完整程式碼會另外出部落格

merge

​ 現在有兩個左偏樹A、B要合併。首先比較A和B的大小,選擇優先值高的作為新樹的根(假設為A),A左子樹不動,B去嘗試和A的右子樹合併,過程與之前相同。

程式碼細節:

int nd[N][2],fa[N]; //nd[]記錄子節點 fa[]並查集
#define ls nd[x][0]
#define rs nd[x][1]
int merge(int x,int y) {
    if(!x||!y) return x+y;
    if(val[x]>val[y]) swap(x,y);
    if(val[x]==val[y]&&x>y) swap(x,y);//題目要求:最小值相同時優先刪除編號最小的,一般可不寫
    fa[rs=merge(rs,y)]=x;
    if(dis[ls]<dis[rs]) swap(ls,rs);
    dis[x]=dis[rs]+1;
    return x;
}
getro

​ 查詢該元素所在堆的根,使用路徑壓縮並查集

int getro(int x) {return fa[x]=fa[x]==x?x:getro(fa[x]);} 
deleteMin

​ 刪除該元素所在堆頂並輸出其值

bool deld[N];//記錄是否刪除,真為已刪除
x=getro(x); cout<<val[x]<<endl;
deld[x]=1; //簡單的刪除操作
fa[x]=fa[ls]=fa[rs]=merge(ls,rs);
//由於使用了路徑壓縮,有很多點fa[]值為x,需要fa[x]=新根,同時ls和rs也需要防止迴圈:fa[x]=ls,fa[ls]=x

斜堆

​ 斜堆和左偏樹非常像,只是不記錄dis值,merge時上面第九行改為不判斷直接swap。這樣降低了儲存空間,達到了平攤意義上的左偏性,但由於不是嚴格的左偏,右子樹可能長度為n= =,使用遞迴寫法可能會爆棧,所以儘量寫非遞迴寫法。(程式碼不寫了跟據上面的改改就行)

隨機堆

​ 隨機堆和斜堆很像= =,就是merge時第九行改為隨機swap,比如: if(rand()%2) swap(ls,rs); 壞處和斜堆類似(不靠譜的堆增加了~qwq)

斐波那契堆

​ 這個是(理論上)真正的好東西~,不過實際運用很少,難寫且常數很大,不過理論複雜度更低

定義

​ 斐波那契堆是一系列具有堆序(上面有提)的有根樹的集合。下面這張圖展示了該堆的結構

​ b圖中展示了具體各個節點儲存的指標,可見一個節點u有指向一個父節點的指標fa,一個指向某一個子節點的指標son。而且u的所有孩子連線成一個環形雙向連結串列稱為u的孩子連結串列,環形連結串列中節點都有left指標和right指標指向左右兄弟。特別的,若孩子連結串列中只有一個節點,那麼它的左右指標都指向自己。另外degree表示u的孩子數目。所有樹的根節點也用環形連結串列連線叫做根連結串列

​ 另外斐波那契堆本身需要存min指標指向具有最小鍵值的樹的根節點(稱作該堆的最小節點),如果有多個最小鍵值,其中一個隨機為最小節點。此外就是堆儲存堆的節點數n。

​ emm具體的演算法過程還是查演算法導論吧,裡面講的很清楚了qwq(真的超清楚了,慢慢看就好),測試題目還是之前的洛谷可並堆模板題,完整程式碼會另外出部落格

​ 這裡我沒有用指標寫,就是用的陣列,可能還有其他原因導致最後常數特別大,測試下來還沒有左偏樹快,但這個理論複雜度要更低一些,另外我刪除任意一點的函式也沒寫,有點寫不動了先留坑吧Orz。

int fdeg[N],fa[N]; //fdeg記錄度數為i的節點 fa是並查集用的陣列
bool vis[N];//記錄該節點是否走過
struct Node{
    int key,degree,fa,son,left,right;//key為鍵值
    bool deld;//是否被刪除(該題特有)
}nd[N];
struct Heap{
    int min,n;
}hep[N];

insert

void insrt(int now,int u) { //將一個節點插入一個堆的根連結串列中
    Node &x=nd[u]; Heap &h=hep[now];
    x.fa=0;
    if(!h.min) {
        x.left=x.right=u;
        h.min=u;
    } else {
        Node &y=nd[h.min],&z=nd[y.right];
        x.left=z.left; x.right=y.right;
        y.right=z.left=u;
        if(y.key>x.key||(y.key==x.key&&h.min>u)) h.min=u;
        // ||之後的部分為該題特有
    }
    h.n++;
}

merge

Heap merge(int u,int v) { //合併兩個堆
    Heap &h1=hep[u],&h2=hep[v],h;
    Node &a1=nd[h1.min],&b1=nd[a1.right];
    Node &a2=nd[h2.min],&b2=nd[a2.left];
    b1.left=a2.left,b2.right=a1.right;
    a1.right=h2.min,a2.left=h1.min;
    h.min=h1.min;
    if(!h1.min || (h2.min&&a2.key<a1.key)) h.min=h2.min;
    h.n=h1.n+h2.n;
    return h;
}
void link(int now,int u,int v) { //將v變成u的子節點 為consoldate輔助
    Node &x=nd[u],&y=nd[v];
    nd[y.left].right=y.right;
    nd[y.right].left=y.left;
    if(x.son) {
        Node &a=nd[x.son],&b=nd[a.right];
        y.left=x.son,y.right=a.right;
        a.right=v,b.left=v;
    } else {
        x.son=v;
        y.left=y.right=v;
    }
    y.fa=u; x.degree++;
}

consoldate

void consoldate(int now) { //將刪除後的堆進行整理合並,防止堆變成連結串列 為deleteMin的後續工作
    Ms(fdeg,0);Ms(vis,0);
    Heap &h=hep[now];
    int cur=h.min;
    while(!vis[cur]) {
        int d=nd[cur].degree,nxt=nd[cur].right; vis[cur]=1;
        while(fdeg[d]) {
            Node &y=nd[fdeg[d]],&x=nd[cur];
            if(x.key>y.key||(x.key==y.key&&cur>fdeg[d])) swap(cur,fdeg[d]);
            // ||後為該題特殊要求
            link(now,cur,fdeg[d]);
            fdeg[d]=0; d++;
        }
        fdeg[d]=cur;
        cur=nxt; 
    }
    h.min=0;
    Fo(i,0,ceil(log2((double)h.n))+1) if(fdeg[i]) {
        int u=fdeg[i]; Node &x=nd[u];
        insrt(now,u); h.n--;
    }
}

deleteMin

int deleteMin(int now) { //刪除並返回最小值,同時對剩下的進行合併(consoldate)
    Heap &h=hep[now];
    Node &x=nd[h.min];
    if(!h.min) return 0;
    int cur=x.son;
    Fo(i,1,x.degree) {
        int nxt=nd[cur].left;
        insrt(now,cur); h.n--;
        nd[cur].fa=0;
        cur=nxt;
    }
    nd[x.left].right=x.right;
    nd[x.right].left=x.left;
    x.deld=1; x.son=0;
    if(x.right==h.min) h.min=0;
    else { 
        h.min=x.right;
        consoldate(now);
    }
    h.n--;
    return x.key;
}

配對堆

​ 暫時留坑吧,整完斐波那契堆以後有點精神疲勞了= =