浅谈左偏树在OI中的应用

Preface

可并堆,一个听起来很NB的数据结构,实际上比一般的就多了一个合并的操作。

考虑一般的堆合并时,当我们合并时只能暴力把一个堆里的元素一个一个插入另一个堆里,这样复杂度将达到\(\log(|A|)+\log(|B|)\),极限数据下显然是要T爆的。

所以我们考虑使用一种性价比最高的可并堆——左偏树,它的思想以及代码都挺简单而且效率也不错。

学习和参考自这里


What is Leftist Tree

左偏树,顾名思义就是像左偏的树,但是这样抽象的表述肯定是不符合我们学OI的人的背板子严谨的态度的。

我们给出一些定义:

  • 外节点:当且仅当这个节点的左子树和右子树中的一个是空节点,注意外节点不是叶子节点
  • 距离(或者是高度?):对于左偏树中的一个节点\(x\),到它的子节点中,离它最近的一个外结点经过的边数称为它的距离,记为\(dist_x\)。特别地,外结点的距离为0,空节点(null)的距离为\(-1\)。

然后跟着这个定义我们可以得出一些性质:

  • 堆性质:对于左偏树中的一个非叶节点应满足堆的性质。如果是大根堆,应满足任意非叶节点的值大于左右孩子(如果有的话)的值。即\(val_x\ge val_{lson(x)},val_{rson(x)}\)
  • 左偏性质:对于左偏树中的任意节点满足它的左子树(如果有的话)的距离大于等于右子树(如果有的话)的距离。即\(dist_{lson(x)}\ge dist_{rson(x)}\)
  • 传递性?:左偏树任意节点的左右儿子(如果有的话)都是一棵左偏树废话

由这几条性质可以发现左偏树是具有左偏性质的堆有序二叉树

同时还有一条不可忽视的终于引理:左偏树中的节点的距离总是满足\(dist_x=dist_{rsom(x)}+1\)

证明:同时由距离的定义以及左偏性质即可得出。


The operator of Leftist Tree

BB了这么就性质啥的抽象东西,是时候讲点真正有用的东西了。

Merge

可并堆的基本操作(也是必备操作)自然是快速地完成合并了,同时合并也是左偏树的灵魂部分,只要说掌握了合并的话就可以直接拿着左偏树大力切题了。

以下假定大根堆的情况,我们假定要合并的两个左偏树(注意单个节点也是左偏树)的根为\(x,y\)

我们首先维护堆的性质,令\(val_x>val_y\),即当值不满足的时候\(\operatorname{swap}(x,y)\)

那么我们发现这时候就要把\(y\)插入\(x\)的子树中了,换句话说,就是要把\(y\)和\(x\)的子树合并。

那么和谁合并呢?考虑我们辛辛苦苦维护的左偏性质,由于右边的链长小于等于左边的,所以为了保证复杂度肯定是直接和\(dist\)小的合并了,于是我们合并\(rson(x)\)和\(y\)

但是插入后右子树的\(dist\)可能会大于左边了这样就变右偏树,我们肯定是不允许的,于是我们判断一下这种情况,如果有就直接交换左右子树(注意直接交换编号即可)即可。

那么什么时候结束呢,当然是当\(x\)或\(y\)中一者为空了啦。所以我们可以比较轻松的得到合并的代码(返回合并的堆的堆顶编号):

inline int merge(int x,int y)
{
    if (!x||!y) return x+y; if (c[x]<c[y]) swap(x,y);
    rc(x)=merge(rc(x),y); if (node[lc(x)].dis<node[rc(x)].dis) swap(lc(x),rc(x));
    node[x].dis=node[rc(x)].dis+1; return x;
}

这里放一张Luogu上找到的动图可以更加生动地理解下:

复杂度的话类似启发式的思想发现是\(O(\log)\)级别的。

push

push的本质就是把一个只有一个节点的左偏树于一颗左偏树合并,因此直接用merge即可。


top

这个直接返回根节点的权值即可。


pop

删除根节点的话可以考虑合并根节点的两个子树,代码

inline void remove(int &x)
{
    x=merge(lc(x),rc(x));
}

板子题:Luogu P3377 【模板】左偏树(可并堆)

这个我们维护小根对的时候再维护一个父节点的信息即可查询两个数是否在同一颗左偏树里了。

CODE

#include<cstdio>
#include<cctype>
using namespace std;
const int N=100005;
struct Leftist_Tree
{
    int ch[2],val,dis,fa;
}node[N];
int n,m,x,y,opt;
inline char tc(void)
{
    static char fl[100000],*A=fl,*B=fl;
    return A==B&&(B=(A=fl)+fread(fl,1,100000,stdin),A==B)?EOF:*A++;
}
inline void read(int &x)
{
    x=0; char ch; while (!isdigit(ch=tc()));
    while (x=(x<<3)+(x<<1)+ch-‘0‘,isdigit(ch=tc()));
}
inline void write(int x)
{
    if (x>9) write(x/10);
    putchar(x%10+‘0‘);
}
inline void swap(int &a,int &b)
{
    int t=a; a=b; b=t;
}
inline int merge(int x,int y)
{
    if (!x||!y) return x+y;
    if (node[x].val>node[y].val||(node[x].val==node[y].val&&x>y)) swap(x,y);
    node[x].ch[1]=merge(node[x].ch[1],y); node[node[x].ch[1]].fa=x;
    if (node[node[x].ch[0]].dis<node[node[x].ch[1]].dis) swap(node[x].ch[0],node[x].ch[1]);
    node[x].dis=node[node[x].ch[1]].dis+1; return x;
}
inline void remove(int x)
{
    node[x].val=-1; node[node[x].ch[0]].fa=node[node[x].ch[1]].fa=0;
    merge(node[x].ch[0],node[x].ch[1]);
}
inline int getfather(int x)
{
    while (node[x].fa) x=node[x].fa; return x;
}
int main()
{
    //freopen("CODE.in","r",stdin); freopen("CODE.out","w",stdout);
    register int i; read(n); read(m); node[0].dis=-1;
    for (i=1;i<=n;++i) read(node[i].val);
    while (m--)
    {
        read(opt); read(x); if (opt^2)
        {
            read(y); int fx=getfather(x),fy=getfather(y);
            if (~node[fx].val&&~node[fy].val&&fx!=fy) merge(fx,fy);
        } else
        {
            int fx=getfather(x); if (!(~node[fx].val)) puts("-1"); else
            write(node[fx].val),putchar(‘\n‘),remove(fx);
        }
    }
    return 0;
}

例题

  • Luogu P1552 [APIO2012]派遣可并堆好题。考虑以每个点为领导者计算答案那么可选的点都在这颗子树中,那么考虑用的人最多我们就贪心的把大的扔掉,回溯时合并两个堆即可。左偏树处理。
  • BZOJ 1367: [Baltic2004]sequence 可并堆好题,一眼看不出。考虑先求不降序列的情况,发现可以对原序列的所有不升序列进行分割,这样必然是取中位数(反证法)。然后用左偏树统计中位数并支持合并即可。

Postscript

可并堆算是介于TG和省选直接的尴尬内容的吧,往年NOIp的话不见出现过。

左偏树虽说在效率上次于配对堆,斐波那契堆这些神仙数据结构,但是它简单的思想以及码量都是很良心的。

还是稍微要掌握一下的吧。

原文地址:https://www.cnblogs.com/cjjsb/p/9768831.html

时间: 2024-10-16 04:46:10

浅谈左偏树在OI中的应用的相关文章

浅谈左偏树

可并堆 可并堆顾名思义就是可以合并的堆. 这里不讲二项堆和斐波那契堆,只讲左偏树. 左偏树 左偏树顾名思义就是向左偏的树. 给每个点定义一个\(dist\),满足下面三个条件: 1.空结点的\(dist\)等于\(-1\) 2.每个结点的左儿子的\(dist\)都大于右儿子的\(dist\) 3.每个结点的\(dist\)都等于右儿子的\(dist+1\) 根据上面这些性质,我们可以推出左偏树中根结点的\(dist\)最大不超过\(logsize\). 合并 左偏树合并非常简单,假设我要合并\(

P3377 【模板】左偏树(可并堆) 左偏树浅谈

因为也是昨天刚接触左偏树,从头理解,如有不慎之处,跪请指教. 左偏树: 什 么是(fzy说)左偏树啊? 前置知识: 左偏树中dist:表示到右叶点(就是一直往右下找,最后一个)的距离,特别的,无右节点的为0. 堆:左偏树是个堆. 关于左偏性质:可以帮助堆合并(研究深了我也不懂的,看代码理解) 对于任意的节点,dist[leftson]>=dist[rightson],体现了左偏性质. 同理可得:对于任意右儿子的父亲节点的dist自然等于右儿子的dist+1喽 关于各种操作: merge: 是插入

可并堆之左偏树浅谈

左偏树是用来快速地合并堆的 正常的堆是一颗完全二叉树,我们用笨方法去合并它: 假设我们要将x和y这两个小根堆合并,我们判断一下如果x的堆顶大于y的堆顶,就交换一下x和y,然后继续合并x的某个子孩子和y. 堆被人们所推广的原因就是因为它的时间复杂度比较稳定,根本原因是堆是一颗完全二叉树 但显然的:这样合并堆并没有保证时间复杂度,也就是说没有维护完全二叉树的形态: 这时候解决的办法之一便是利用左偏树: 它比普通的堆多了一个性质:向左偏: 注意,这里的向左偏并不是指子树的大小向左偏,而是最大深度向左偏

左偏树

概要:左偏树是具有左偏性质的堆有序二叉树,它相比于优先队列,能够实现合并堆的功能. 先仪式型orzorzozr国家集训队论文https://wenku.baidu.com/view/515f76e90975f46527d3e1d5.html 左偏树的节点定义: 1 struct node { 2 int lc, rc, val, dis; 3 } LTree[maxn]; 左偏树的几个基本性质如下: 节点的键值小于等于它的左右子节点的键值 节点的左子节点的距离不小于右子节点的距离 节点的距离等于

poj 3016 K-Monotonic 左偏树 + 贪心 + dp

//poj 3016 K-Monotonic//分析:与2005年集训队论文黄源河提到的题目类似,给定序列a,求一序列b,b不减,且sigma(abs(ai-bi))最小.//思路:去除左偏树(大根堆)一半的节点(向上取整),让左偏树的根节点上存放中位数:每个左偏树的根节点表示一个等值区间//在本题中,我们将一段区间 与 一颗左偏树等同:将求调整给定数列 vi 为不减序列的代价 与 求取数列 bi 等同 1 #include"iostream" 2 #include"cstd

浅析左偏树的性质及其应用

本文是看了黄源河的论文后才写的 如果本人有哪些地方写得不对的,希望各位大佬改正ORZ 学习C++的大佬应该都会优先队列(原谅我的菜,我连priority_queue都不会拼) 左偏树说到底就是一个升级版的堆 因为左偏树拥有所有堆拥有的功能比如说插入一个节点,取出堆顶和删除堆顶 我们的左偏树的优秀到底体现在哪呢? 左偏树可以合并两个堆!!! 如果我们用普通的做法合并两个堆是需要O(N)的时间 那么如果合并操作非常多 那么堆就不在实用了 先来规定左偏树的一些概念 外节点:一个没有右儿子的节点成为外节

【左偏树】【APIO】Dispatching

2809: [Apio2012]dispatching Time Limit: 10 Sec Memory Limit: 128 MB Submit: 1932 Solved: 967 Description 在一个忍者的帮派里,一些忍者们被选中派遣给顾客,然后依据自己的工作获取报偿.在这个帮派里,有一名忍者被称之为 Master.除了 Master以外,每名忍者都有且仅有一个上级.为保密,同时增强忍者们的领导力,所有与他们工作相关的指令总是由上级发送给他的直接下属,而不允许通过其他的方式发送.

左偏树(Leftist Heap/Tree)简介及代码

左偏树是一种常用的优先队列(堆)结构.与二叉堆相比,左偏树可以高效的实现两个堆的合并操作. 左偏树实现方便,编程复杂度低,而且有着不俗的效率表现. 它的一个常见应用就是与并查集结合使用.利用并查集确定两个元素是否在同一集合,利用左偏树确定某个集合中优先级最高的元素. 1 #include <cstdio> 2 #include <cstring> 3 #include <algorithm> 4 5 template <class T> 6 struct H

zoj 2334 Monkey King/左偏树+并查集

原题链接:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=1389 大致题意:N只相互不认识的猴子(每只猴子有一个战斗力值) 两只不认识的猴子之间发生冲突,两只猴子会分别请出它们认识的最强壮的 猴子进行决斗.决斗之后这,两群猴子都相互认识了. 决斗的那两只猴子战斗力减半...有m组询问 输入a b表示猴子a和b发生了冲突,若a,b属于同一个集合输出-1 否则输出决斗之后这群猴子(已合并)中最强的战斗力值... 具体思路:用并查