1. 程式人生 > >左偏樹學習筆記

左偏樹學習筆記

sin 定義 ems def 習題 路徑壓縮 reat line its

左偏樹學習筆記

具體印象

普通的二叉堆只能實現普通的堆的功能。

如果需要支持合並操作,就需要使用可並堆,左偏樹就是一種好寫的可並堆。

左偏樹是一顆二叉樹,由於特殊性質,整棵樹會集中在左邊,所以叫做左偏樹。

左偏給堆的合並創造了很優秀的復雜度。

技術分享圖片

上面的圖片來自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!

左偏樹學習筆記