1. 程式人生 > 資訊 >曝華為 MatePad 11 平板電腦售價 3000 元左右:7 月釋出,搭載驍龍 865、鴻蒙 HarmonyOS 2

曝華為 MatePad 11 平板電腦售價 3000 元左右:7 月釋出,搭載驍龍 865、鴻蒙 HarmonyOS 2

左偏樹,是一種可以在 \(\Theta(\log_2 n)\) 複雜度內合併的一種資料結構。

它是以二叉樹的形態來維護堆的性質,由此也延伸出很多別的操作。

接下來本文所述堆及其操作均預設為小根堆。

\[\]

一些約定

  • \(v_i\) 節點 \(i\) 的權值。

  • \(l_i,r_i\) 節點 \(i\) 的左右兒子。

  • \(d_i\) 節點 \(i\) 到其最近外節點的距離。

  • \(f_i\) 節點 \(i\) 的父節點。

什麼是外節點:一個左兒子或右兒子是空節點的節點。

\[\]

左偏樹的性質

堆性質

作為堆式資料結構顯然有堆的性質,即對任意一個樹中的點 \(i\)\(v_i<v_{l_i},v_i<v_{r_i}\)

但是任意一個節點左右兒子的大小並不確定,這也不在我們的維護範圍之內。

左偏性質

左偏,就是對於任意一個樹中的點 \(i\)\(d_{l_i}>d_{r_i}\)

另外的,對於任意一個樹中的點 \(i\)\(d_i=d_{r_i}+1\)

\[\]

左偏樹的操作

合併操作

左偏樹最核心的操作,其餘的操作也均以這個操作為基礎。

假設我們要合併分別以 \(a,b\) 為根節點的兩棵左偏樹。

首先,若其中一棵樹的根為空節點,則另一棵樹的根成為新樹的根。

若兩個樹根均存在,那麼權值更小的必定成為新的樹根。

這裡我們假設 \(v_a<v_b\),即 \(a\) 為新樹的根。為了避免分類討論,當 \(v_a>v_b\)

時,我們可以將其交換。

此時我們再令 \(b\)\(a\) 的右兒子進行合併,重新比較其大小。如此不斷比較下去直到遇到空節點或葉節點為止。

這樣遞迴返回每次合併的根節點的值,重置此樹的相關節點。

另外,由於是對右兒子進行合併,所以可能合併後的樹不再滿足左偏性質。此時我們交換左右子樹。

我們要時刻保證對於任意一個樹中的節點 \(i\)\(d_i=d_{r_i}+1\),所以我們重新規定一下右兒子的距離即可。

實現也比較容易。

int merge(int x,int y){
  if(!x||!y) return x+y;
  if(v[y]<v[x]) swap(x,y);
  rs=merge(rs,y);
  if(dis[ls]<dis[rs])swap(ls,rs);
  dis[x]=dis[rs]+1;
  return x;
}

還有一種寫法是隨機合併,引入隨機數來判斷是否交換左右節點。

這樣做可能會使樹不滿足左偏性質,但是省去了距離的相關計算,且複雜度不變。

取最值操作

由於左偏樹的堆性質,它的最值肯定在堆頂。那麼就直接不斷跳父節點就行了。

但是我們發現暴跳太慢了。我們可以藉助並查集對其路徑壓縮。

對於同一棵左偏樹中的點用並查集連線到一起,並欽定其祖先節點為堆頂。

於是就可以在 \(\Theta{\log_2 n}\) 的時間內快速找出最值了。

刪除操作

刪除操作只針對堆頂節點來說。

刪除節點需要重置其資訊(左右兒子和距離)。

另外,刪除此節點之後,其真正的左右兒子並沒有被刪除,但是現在他們都沒有了父親(?),所以我們需要合併這兩個點。

fa[lson[x]]=fa[rson[x]]=fa[x]=merge(lson[x],rson[x]);

注意這個節點本身的父親也要指向合併後的根節點。

因為路徑壓縮後刪除此節點,此節點的父親指向的是自己,若不更改可能會導致後面出各種問題。

插入節點

等同於將其與一棵大小為一的堆合併。

此時要保證插入的這個點的各個資訊已經處理完善。

全樹加/減/乘一個值

維護懶標記,從根節點開始不斷向下穿即可。具體是在刪除/合併訪問兒子時下傳。

由於只能單個傳,所以速度較慢。

當然不止這三種操作,可以打標記且不改變相對大小的操作都可以。

\[\]

例題

Monkey King

給定初始的 \(n\) 個大小為一的堆及其權值,再給定 \(m\) 次詢問。
每次詢問 x y,表示將 \(x\)\(y\) 所在的堆的堆頂權值變為原來的一半後合併。
對於每次操作,輸出新堆的堆頂權值。
\(n,m\le 100000\)

一開始的思路是,每次新建兩個權值分別為兩個堆頂權值一半的點,先將這兩個點與這兩個堆合併,再刪除堆頂,最後合併兩個堆。

至於權值方面,由於我懶得寫大根堆,於是就將權值的相反數插入堆中建小根堆。

注意奇數的整除是下取整,所以對於奇數 \(a\),要將 a>>=1 寫成 a=-((-a)>>1)

計算下來若每次新建兩個點,那麼總共就是 \(2\times m\) 個,加上原來的應該不超過 \(300000\) 個點。

但不知道為什麼總是 MLE,至今沒找到原因(

所以我換了寫法,不再新建節點,而是先刪除兩個堆頂,再在每個堆中加入新點,只不過點的編號還是原來堆頂的。

最後合併兩個堆就做完了。

程式碼和板子的差不多。

#include<queue>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 100100
#define INF 0x3f3f3f3f
//#define int long long

using namespace std;

int n,m,cnt;

int read(){
  int s=0,w=1;char ch=getchar();
  while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
  while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
  return s*w;
}

namespace LIT{
  #define ls lson[x]
  #define rs rson[x]
  int fa[maxn],dis[maxn];
  int lson[maxn],rson[maxn];
  
  struct node{
    int pos,val;
    bool operator < (const node &b) const{
      return val^b.val?val<b.val:pos<b.pos;  
    } 
  }v[maxn];
  
  int findf(int x){
    return fa[x]==x?x:fa[x]=findf(fa[x]);
  }
  
  int merge(int x,int y){
    if(!x||!y) return x+y;
    if(v[y]<v[x]) swap(x,y);
    rs=merge(rs,y);
    if(dis[ls]<dis[rs])swap(ls,rs);
    dis[x]=dis[rs]+1;
    return x;
  }
}

using namespace LIT;

signed main(){
  dis[0]=-1;
  while(scanf("%d",&n)!=EOF){
    for(int i=1;i<=n;i++){
      lson[i]=rson[i]=dis[i]=0;
      v[i].val=read()*-1;
      v[i].pos=fa[i]=i;
    }
    m=read();cnt=n;
    for(int i=1,x,y;i<=m;i++){
      x=findf(read());y=findf(read());
      if(x==y){printf("-1\n");continue;}
      v[x].val=-((-v[x].val)>>1);
      int rt1=merge(lson[x],rson[x]);
      lson[x]=rson[x]=dis[x]=0;
      int now1=fa[rt1]=fa[x]=merge(rt1,x);
      v[y].val=-((-v[y].val)>>1);
      int rt2=merge(lson[y],rson[y]);
      lson[y]=rson[y]=dis[y]=0;
      int now2=fa[rt2]=fa[y]=merge(rt2,y);
      fa[now1]=fa[now2]=merge(now1,now2);
      printf("%d\n",-v[fa[now1]].val);
    }
  }
  return 0;
}