首页 > 其他 > 详细

左偏树学习笔记

时间:2019-02-04 20:35:29      阅读:201      评论:0      收藏:0      [点我收藏+]

左偏树学习笔记

具体印象

普通的二叉堆只能实现普通的堆的功能。

如果需要支持合并操作,就需要使用可并堆,左偏树就是一种好写的可并堆。

左偏树是一颗二叉树,由于特殊性质,整棵树会集中在左边,所以叫做左偏树。

左偏给堆的合并创造了很优秀的复杂度。

技术分享图片

上面的图片来自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!

左偏树学习笔记

原文:https://www.cnblogs.com/Garen-Wang/p/10352099.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!