左偏樹學習筆記
左偏樹學習筆記
具體印象
普通的二叉堆只能實現普通的堆的功能。
如果需要支持合並操作,就需要使用可並堆,左偏樹就是一種好寫的可並堆。
左偏樹是一顆二叉樹,由於特殊性質,整棵樹會集中在左邊,所以叫做左偏樹。
左偏給堆的合並創造了很優秀的復雜度。
上面的圖片來自luoguP3378題解區@lolte dalao。隨便盜來的
定義
左偏樹有一個npl值,但是我不習慣這麽叫,直接把它叫做dist。
定義0節點的dist值為-1。同時定義每一個節點的dist值為它右兒子的dist值\(+1\)。
上面那張圖,節點外的數字就是該節點的dist值。
可以發現,左偏樹的dist值都出奇的小,結合下面的核心操作,你就知道怎麽搞出這麽優秀的復雜度了。
核心操作:合並(merge)
左偏樹有且僅有這麽一個操作:合並(merge)。
合並操作比較簡單:遞歸弄到主堆的最右節點,與次堆合並,之後再判斷左右子樹的dist值大小,把dist值大的子樹作為左子樹,滿足其左偏的性質。
上面的圖片來自luoguP3377題解區@遠航之曲 dalao。亂盜圖系列
核心代碼直接po上來:
int merge(int x, int y) { if(x == 0 || y == 0) return x + y;// 只要一個為空,就返回另外一個 if(val[x] > val[y]) std::swap(x, y);// 滿足小根堆的性質(因題而異) else if(val[x] == val[y] && x > y) std::swap(x, y);// 滿足下標小的先(題目特殊要求) ch[x][1] = merge(ch[x][1], y);// 遞歸用右子樹合並 fa[ch[x][1]] = x;// 認爸爸 if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);// 滿足左偏樹性質 dist[x] = dist[ch[x][1]] + 1;// 根據定義計算dist return x; }
其他操作
其他操作都是建立在merge操作之上的。比如:
在原堆上添加一個新數
把這個新數視為一個堆,直接用merge操作跟原堆合並即可。
刪除堆頂
只需要獲得堆頂,讓它兩個兒子都不認它作爸爸,把這兩個新堆一起合並即可。
註意事項
操作模板是給你\(n\)個堆,再進行這些堆的操作,而不是給你插入\(n\)個數。這個是要註意的。
左偏樹的路徑壓縮問題
沒有路徑壓縮的左偏樹在P3377是會T最後一個點的。
但是實際發現好多可並堆的題並不卡不路徑壓縮的代碼。
如果要路徑壓縮也不是問題,但是需要更改一下上面fa
數組的定義了。
直接從find函數就可以明白改在哪裏了:
int find(int x) {// 原 if(fa[x] == x) return x; return fa[x] = find(fa[x]); } int find(int x) {// 新 if(fa[x] == x) return x; return fa[x] = find(fa[x]); }
其實就是一個並查集。
下面是有路徑壓縮的左偏樹,與沒有路徑壓縮的代碼不同的地方已經標出來了:
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 11:14:55 AM CST
@File Name: P3377.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
#define ll long long
const int maxn = 100005;
int dist[maxn], ch[maxn][2], fa[maxn], val[maxn];
int n, m;
int find(int x) {
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);// 經典路徑壓縮
}
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;
if(val[x] > val[y]) std::swap(x, y);
else if(val[x] == val[y] && x > y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
fa[x] = fa[ch[x][0]] = fa[ch[x][1]] = x;// 三個節點的fa都是x
dist[x] = dist[ch[x][1]] + 1;
return x;
}
void pop(int x) {
val[x] = -1;
fa[ch[x][0]] = ch[x][0]; fa[ch[x][1]] = ch[x][1];// fa是它們自己
fa[x] = merge(ch[x][0], ch[x][1]);// merge的返回值需要用到
}
int main() {
cin >> n >> m;
dist[0] = -1;
for(int i = 1; i <= n; i++) {
cin >> val[i];
fa[i] = i;// 並查集的初始化
}
while(m--) {
int opt, x, y; cin >> opt;
if(opt == 1) {
cin >> x >> y;
if(val[x] == -1 || val[y] == -1) continue;
x = find(x), y = find(y);
if(x == y) continue;
fa[x] = fa[y] = merge(x, y);// merge的返回還是要用到
} else if(opt == 2) {
cin >> x;
if(val[x] == -1) cout << -1 << endl;
else {
x = find(x);
cout << val[x] << endl;
pop(x);
}
}
}
return 0;
}
練習題
1. P3378 【模板】堆
可並堆可以實現普通堆的操作,我自己認為還比二叉堆好寫點。
唯一要註意的就是要yy出一個新堆,跟自己維護出的主堆合並就是了。
代碼走一波:(沒有路徑壓縮)
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 12:30:20 PM CST
@File Name: P3378.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
using std::cin;
using std::cout;
using std::endl;
const int maxn = 1000005;
int dist[maxn], fa[maxn], ch[maxn][2], val[maxn];
int n, m;
int mian;
int find(int x) {
while(fa[x]) x = fa[x];
return x;
}
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;
if(val[x] > val[y]) std::swap(x, y);
else if(val[x] == val[y] && x > y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
fa[ch[x][1]] = x;
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
dist[x] = dist[ch[x][1]] + 1;
return x;
}
int main() {
dist[0] = -1;
cin >> m;
for(int i = 1; i <= m; i++) {
int opt, x;
cin >> opt;
if(opt == 1) {
cin >> x;
val[++n] = x;
mian = merge(mian, n);
} else if(opt == 2) {
cout << val[mian] << endl;
} else if(opt == 3) {
val[mian] = -1;
fa[ch[mian][0]] = fa[ch[mian][1]] = 0;
mian = merge(ch[mian][0], ch[mian][1]);
}
}
return 0;
}
2. P1456 Monkey King
文體兩開花
顯然把每個孤獨的猴子看成堆,每次就合並咯。這就是可並堆。
這裏就有一個問題:戰鬥力減半如何操作?
其實也非常暴力:把戰鬥的猴子先刪了,再插入一個減半的戰鬥力即可。
代碼走一波:(沒有路徑壓縮)
/*************************************************************************
@Author: Garen
@Created Time : Mon 04 Feb 2019 02:16:41 PM CST
@File Name: P1456.cpp
@Description:
************************************************************************/
#include<bits/stdc++.h>
#define ll long long
const int maxn = 100005;
int val[maxn], dist[maxn], fa[maxn], ch[maxn][2];
int n, m;
void clearlove() {
memset(dist, 0, sizeof dist);
memset(fa, 0, sizeof fa);
memset(ch, 0, sizeof ch);
}
int find(int x) {
while(fa[x]) x = fa[x];
return x;
}
int merge(int x, int y) {
if(x == 0 || y == 0) return x + y;
if(val[x] < val[y]) std::swap(x, y);
else if(val[x] == val[y] && x < y) std::swap(x, y);
ch[x][1] = merge(ch[x][1], y);
fa[ch[x][1]] = x;
if(dist[ch[x][0]] < dist[ch[x][1]]) std::swap(ch[x][0], ch[x][1]);
dist[x] = dist[ch[x][1]] + 1;
return x;
}
int fight(int x, int y) {
x = find(x), y = find(y);
if(x == y) return -1;
int temp1 = val[x], temp2 = val[y];
val[x] >>= 1;
fa[ch[x][0]] = fa[ch[x][1]] = 0;
int temp_1 = merge(ch[x][0], ch[x][1]);
ch[x][0] = ch[x][1] = 0;
int newtemp_1 = merge(temp_1, x);
val[y] >>= 1;
fa[ch[y][0]] = fa[ch[y][1]] = 0;
int temp_2 = merge(ch[y][0], ch[y][1]);
ch[y][0] = ch[y][1] = 0;
int newtemp_2 = merge(temp_2, y);
int sb = merge(newtemp_1, newtemp_2);
return val[sb];
}
int main() {
while(scanf("%d", &n) == 1) {
clearlove();
dist[0] = -1;
for(int i = 1; i <= n; i++) scanf("%d", &val[i]);
scanf("%d", &m);
while(m--) {
int x, y; scanf("%d%d", &x, &y);
printf("%d\n", fight(x, y));
}
}
return 0;
}
最後
Happy Spring Festival!
左偏樹學習筆記