左偏樹——楊子曰數據結構

左偏樹——楊子曰數據結構

先扔出一道題(【洛谷】P3377 【模板】左偏樹(可並堆)):

題目描述
如題,一開始有N個小根堆,每個堆包含且僅包含一個數。接下來需要支持兩種操作:

操作1: 1 x y 將第x個數和第y個數所在的小根堆合併(若第x或第y個數已經被刪除或第x和第y個數在用一個堆內,則無視此操作)

操作2: 2 x 輸出第x個數所在的堆最小數,並將其刪除(若第x個數已經被刪除,則輸出-1並無視刪除操作)

輸入格式
第一行包含兩個正整數N、M,分別表示一開始小根堆的個數和接下來操作的個數。

第二行包含N個正整數,其中第i個正整數表示第i個小根堆初始時包含且僅包含的數。

接下來M行每行2個或3個正整數,表示一條操作,格式如下:

操作1 : 1 x y

操作2 : 2 x

輸出格式
輸出包含若干行整數,分別依次對應每一個操作2所得的結果。

輸入輸出樣例

輸入

5 5
1 5 4 2 3
1 1 5
1 2 5
2 2
1 4 2
2 2

輸出

1
2

說明/提示
當堆裏有多個最小值時,優先刪除原序列的靠前的,否則會影響後續操作1導致WA。

時空限制:1000ms,128M

數據規模:

對於30%的數據:N<=10,M<=10

對於70%的數據:N<=1000,M<=1000

對於100%的數據:N<=100000,M<=100000

樣例說明:

初始狀態下,五個小根堆分別爲:{1}、{5}、{4}、{2}、{3}。

第一次操作,將第1個數所在的小根堆與第5個數所在的小根堆合併,故變爲四個小根堆:{1,3}、{5}、{4}、{2}。

第二次操作,將第2個數所在的小根堆與第5個數所在的小根堆合併,故變爲三個小根堆:{1,3,5}、{4}、{2}。

第三次操作,將第2個數所在的小根堆的最小值輸出並刪除,故輸出1,第一個數被刪除,三個小根堆爲:{3,5}、{4}、{2}。

第四次操作,將第4個數所在的小根堆與第2個數所在的小根堆合併,故變爲兩個小根堆:{2,3,5}、{4}。

第五次操作,將第2個數所在的小根堆的最小值輸出並刪除,故輸出2,第四個數被刪除,兩個小根堆爲:{3,5}、{4}。

故輸出依次爲1、2。


說白了就是讓你維護好幾個堆,可以求最值,彈出,合併

讓我們用一個nb的數據結構——左偏樹,來解決這道題


首先,左偏樹長這樣:
在這裏插入圖片描述

哦,不好意思o( ̄┰ ̄*)ゞ,放錯圖了,是這張:
在這裏插入圖片描述
這棵樹明顯地向左偏了呀!這就是爲什馬它叫左偏樹

先來說一下對於這棵樹上的每個結點我們要記錄什麼:

  • ls:左兒子
  • rs:右兒子
  • v:權值
  • f:用來維護並查集的東西,用它來找到當前結點的根
  • dis:左偏樹中最最重要的東西,就是從當前結點出發,不停往右兒子走,能走多遠

我們把dis標上來給大家瞅瞅(沒有標的dis是0):

在這裏插入圖片描述
然後我們來曰一曰左偏樹需要滿足的性質:

  • 滿足小根堆或者大根堆的性質
  • 對於每個節點dis[ls]dis[rs]dis[ls] \geq dis[rs],這也就是它左偏的性質

對於左偏樹上的每一個結點,dis[ls]dis[rs]dis[ls]\geq dis[rs],這就是它左偏的原因

那麼我們怎麼來維護這個dis捏?特別簡單,對於結點i的dis:dis[i]=dis[rs[i]]+1,特別好理解!

在開始講各種操作之前,我們先要說明一個事情,根結點的dis不會超過log n,也就是整棵樹最右邊那條鏈的長度不會超過log n:
我們假設最右邊那條鏈上的結點個數爲x,我們手動模一下會發現:爲了維護每個dis,點最少最少也需要大致上構成一個滿二叉樹So,最右邊那條鏈一定是log級別的,這也就是它複雜度如此優秀的原因


好的,接下來我們講一講怎麼解決上面的那道題目:

首先,我們要用一個並查集來維護每個結點所在左偏樹的根的編號,每個結點記錄的f值就是用來左並查集的,這我就不多講了

  1. 合併(merge)
    在這裏插入圖片描述
    我們需要維護堆的性質,So,我們就比較一下我們要合併的兩棵樹的根的權值由於是小根堆,那我們就要讓權值小的點成爲新的根,然後合併新的根的右兒子和另一棵樹。比如上面那個圖,經過一番比較以後發現v[1]<v[13],那麼我們就要讓1號結點成爲新的根,然後遞歸地去合併以3爲根節點的樹和13爲根節點的樹,並把這個子問題合併後新的根賦給1,如下圖:
    在這裏插入圖片描述
    然後就變成了一個新的子問題,不斷遞歸下去,直到兩顆子樹右一顆子樹空了。
    當然,這樣合併完以後整條路徑上的dis可能都不準了,So,我們要在回溯的時候把dis數組更新一下。
    “那如果驚悚的發現某個結點左兒子的dis小於了右兒子的dis,也就是不滿足左偏樹的性質了怎麼辦?”
    “不要緊張,直接更換它的左右兒子!”
    完事。
int merge(int l,int r){
	if (l==0 || r==0) return l+r;
	if (t[l].v>t[r].v || (t[l].v==t[r].v && l>r)) swap(l,r);
	t[l].rs=merge(t[l].rs,r);
	if (t[t[l].ls].dis<t[t[l].rs].dis) swap(t[l].ls,t[l].rs);
	t[t[l].ls].f=t[t[l].rs].f=t[l].f=l;
	t[l].dis=t[t[l].rs].dis+1;
	return l;
}
  1. 彈出/刪除(del)
    這個操作實在是太簡單了,我們要彈出這顆左偏樹的根,我們只要完全忽視根節點,把它的兩棵子樹合併,完事。
    (不過要注意一點,假設我們要刪掉x,由於原來x的有些後代的f數組指向了x,而x被刪掉後他們應該指向的是x的兩個兒子合併後新的根,那我們這樣來處理:雖然x被刪了,我們把x的f值附成兩個兒子合併後新的根,這樣它的後代在並查集中找根的時候就可以找到正確的根了)
    在這裏插入圖片描述
void del(int x){
	t[x].v=-1;
	t[t[x].ls].f=t[x].ls;
	t[t[x].rs].f=t[x].rs;
	t[x].f=merge(t[x].ls,t[x].rs);
}
  1. 查詢最值(query)
    這個就更簡單了,我們用維護的並查集找到這棵樹的根,輸出根上的權值,完事。
int query(int x){
	int fx=gf(x);
	return t[fx].v;
}

由於在合併的時候我們只會往右兒子走,而我們又說明最右邊那條鏈的長度是log級別的,自然它的時間複雜度也是O(log n)的。

OK,完事


c++代碼(洛谷 P3377):

#include<bits/stdc++.h>
using namespace std;

const int maxn=150005;

struct Tr{
	int ls,rs,dis,v,f;
}t[maxn];

int n,m;

int gf(int x){
	return t[x].f==x?x:t[x].f=gf(t[x].f);
}

int merge(int l,int r){
	if (l==0 || r==0) return l+r;
	if (t[l].v>t[r].v || (t[l].v==t[r].v && l>r)) swap(l,r);
	t[l].rs=merge(t[l].rs,r);
	if (t[t[l].ls].dis<t[t[l].rs].dis) swap(t[l].ls,t[l].rs);
	t[t[l].ls].f=t[t[l].rs].f=t[l].f=l;
	t[l].dis=t[t[l].rs].dis+1;
	return l;
}

void del(int x){
	t[x].v=-1;
	t[t[x].ls].f=t[x].ls;
	t[t[x].rs].f=t[x].rs;
	t[x].f=merge(t[x].ls,t[x].rs);
}

int query(int x){
	int fx=gf(x);
	return t[fx].v;
}

int main(){
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++){
		scanf("%d",&t[i].v);
		t[i].f=i;
	}
	while(m--){
		int opt;
		scanf("%d",&opt);
		if (opt==1){
			int x,y;
			scanf("%d%d",&x,&y);
			int fx=gf(x),fy=gf(y);
			if (t[x].v==-1 || t[y].v==-1) continue;
			if (fx==fy) continue;
			t[fx].f=t[fy].f=merge(fx,fy);
		}
		else{
			int x;
			scanf("%d",&x);
			if (t[x].v==-1){
				puts("-1");
				continue;
			}
			printf("%d\n",query(x));
			del(gf(x));
		}
	}
	return 0;
}

於HG機房

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章