<更新提示>

<第一次更新>

<第二次更新>新增一道例题


<正文>

左偏树 Leftist Tree

这是一个由堆(优先队列)推广而来的神奇数据结构,我们先来了解一下它。

简单的来说,左偏树可以实现一般堆的所有功能,如查询最值,删除堆顶元素,加入新元素等,时间复杂度也均相等,与其不同的是,左偏树还可以在\(O(log_2n)\)的时间之内实现两个堆的合并操作,这是一般的堆无法做到的。

特点

当然,左偏树是一个树形数据结构,我们需要像线段树一样使用一个结构体来记录每一个节点上的若干信息,以便于进行查询,合并等操作,具体如下:

  • 1.\(val\)值,代表该编号元素的权值
  • 2.\(dis\)值,代表该节点到叶子节点的最短距离
  • 3.\(l\)值,代表该节点的左儿子编号
  • 4.\(r\)值,代表该节点的右儿子编号
  • 5.\(f\)值,代表该节点的父亲编号

对于这样的二叉树结构,我们还是用数组中父子二倍的方式来储存的,当然当某个节点\(x\)为叶子节点时,满足\(l(x)=r(x)=0\),当某个节点\(x\)为根节点时,满足\(f(x)=x\)(类似于并查集,作用稍后会讲)。

\(Code:\)

struct LeftistTree
{
int dis,val,l,r,f;
#define dis(x) (tree[x].dis)
#define val(x) (tree[x].val)
#define l(x) (tree[x].l)
#define r(x) (tree[x].r)
#define f(x) (tree[x].f)
}tree[N];

性质

左偏树需要具有以下几点重要的性质,请读者牢记。

  • 1.堆性质:\(val(x)\leq val(l(x))\)且\(val(x)\leq val(r(x))\)(以小根堆为例)
  • 2.左偏性质\(1\):对于任意\(x\),\(dis(r(x))\leq dis(l(x))\)
  • 3.左偏性质\(2\):对于任意\(x\),\(dis(x)=dis(r(x))+1\)

对于堆性质,相信读者能够很好地理解。对于两个左偏性质,读者在此完全可以先认为:左偏树维护了一棵树左偏的形态,也就是说,对于每一个棵子树,尽量使右儿子离根节点近,即这棵树左重右轻。

还有一个有关时间复杂度的性质,证明如下:

  • 节点数为\(n\)的左偏树,其最大\(dis\)值至多为\(log_2(n+1)-1\)

证明:

对于一棵最大距离为\(k\)的左偏树,至少有\(2^{k+1}-1\)个节点,这是可以由二叉树的基本性质得到的。

由此,我们得到:$$n\geq 2^{k+1}-1\⇒n+1\geq 2^{k+1}\⇒log_2{(n+1)}\geq k+1\⇒k\leq log_2{(n+1)-1}$$

证毕。

合并 (merge)

合并操作是左偏树最重要的操作,必须深刻理解。

假设我们已经得到了两个左偏树,我们考虑如果将其合并。我们要使整棵树的时间复杂度得到保证,也就是说要让树的层数尽量小。由于我们维护了每一个右儿子\(dis\)值尽量的小,所以我们可以将一个合并问题\(merge(x,y)\)转化为合并一棵树的右儿子和另一棵树,即\(merge(r(x),y)\)。当然,为了维护堆性质,我们还要保证\(val(x)<val(y)\)。

完成合并后,由于根节点的右儿子可能发生了变化,所以我们要对右儿子的父亲重新进行更新。

为了维护左偏性质\(1\)和\(2\),以便之后的合并操作,我们可能还需要再对左右子树进行交换,并顺带更新\(dis\)值,即令\(dis(x)=dis(r(x))+1\)。

由之前的证明可知,节点数为\(n\)的左偏树,其最大\(dis\)值至多为\(log_2(n+1)-1\),那么我们的合并操作的最大时间复杂度即为$$O(\max_{i \in tree(x)}{dis_i}+\max_{j \in tree(y)}{dis_j})\=O(2log_2(n+1)-2)=O(log_2n)$$

,符合我们的需要。

\(Code:\)

inline int merge(int x,int y)//返回值的含义为合并后新树的根节点编号
{
if(!x||!y)return x|y;//如果x,y有一棵是空树,返回另一棵的编号
if(val(x)>val(y)||(val(x)==val(y)&&x>y))//维护堆性质,把权值大的合并到权值小的上
swap(x,y);
r(x)=merge(r(x),y);//递归合并x的右子树与y
f(r(x))=x;//更新右子树父亲
if(dis(l(x))<dis(r(x)))//维护左偏性质1
swap(l(x),r(x));
dis(x)=dis(r(x))+1;//维护左偏性质2
return x;
}

取出堆顶 (find)

除了合并外,左偏树当然要实现它的本职工作,查询最值。

由于我们已经维护好了左偏树,只要直接输出树根的权值即为最小值。

\(Code:\)

inline int find(int x)
{
return f(x)==x?x:f(x)=find(f(x));
//由于树的最大深度未知,直接查询可能会导致时间复杂度退化为O(n)
//所以要用并查集的路径压缩写法
}
int Min=val(find(p));//查询节点p所在左偏树的最小值,p已经被删除则返回-1

删除堆顶元素 (remove)

当然,删除最值元素的操作也是可以简易地实现的。我们只需要将该节点的权值赋为\(-1\),并将以其左右儿子为根的两棵子树合并即可。

值得注意的是,我们还要将删除节点的父亲节点赋为两棵子树合并后的节点编号。由于我们查询最值用的是路径压缩,所以树中某些节点的父亲可能已经直接压缩到了当前节点,而现在当前节点又要被删除,所以我们要将当前节点的父亲再赋值为删去后新树的根节点,在查询时可以再找回新树,以避免查询错误。

\(Code:\)

inline void remove(int x)
{
val(x)=-1;//清空权值
f(l(x))=l(x);f(r(x))=r(x);//重置左右儿子的父亲
f(x)=merge(l(x),r(x));//将父亲重新赋值为新树的根节点
}

至此,左偏树的基本代码已经实现,下面通过洛谷的一道模板题给出代码。

左偏树

Description

如题,一开始有N个小根堆,每个堆包含且仅包含一个数。接下来需要支持两种操作:

操作1: 1 x y 将第x个数和第y个数所在的小根堆合并(若第x或第y个数已经被删除或第x和第y个数在用一个堆内,则无视此操作)

操作2: 2 x 输出第x个数所在的堆最小数,并将其删除(若第x个数已经被删除,则输出-1并无视删除操作)

Input Format

第一行包含两个正整数N、M,分别表示一开始小根堆的个数和接下来操作的个数。

第二行包含N个正整数,其中第i个正整数表示第i个小根堆初始时包含且仅包含的数。

接下来M行每行2个或3个正整数,表示一条操作,格式如下:

操作1 : 1 x y

操作2 : 2 x

Output Format

输出包含若干行整数,分别依次对应每一个操作2所得的结果。

Sample Input

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

Sample Output

1
2

\(Code:\)

#include<bits/stdc++.h>
using namespace std;
#define filein(str) freopen(str".in","r",stdin)
#define fileout(str) freopen(str".out","w",stdout)
const int N=100000+20;
struct LeftistTree
{
int dis,val,l,r,f;
#define dis(x) (tree[x].dis)
#define val(x) (tree[x].val)
#define l(x) (tree[x].l)
#define r(x) (tree[x].r)
#define f(x) (tree[x].f)
}tree[N];
int n,m;
inline int find(int x)
{
return f(x)==x?x:f(x)=find(f(x));
}
inline int merge(int x,int y)
{
if(!x||!y)return x|y;
if(val(x)>val(y)||(val(x)==val(y)&&x>y))
swap(x,y);
r(x)=merge(r(x),y);
f(r(x))=x;
if(dis(l(x))<dis(r(x)))
swap(l(x),r(x));
dis(x)=dis(r(x))+1;
return x;
}
inline void remove(int x)
{
val(x)=-1;
f(l(x))=l(x);f(r(x))=r(x);
f(x)=merge(l(x),r(x));
}
inline void input(void)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
f(i)=i;
scanf("%d",&val(i));
}
int op,x,y;dis(0)=-1;
for(int i=1;i<=m;i++)
{
scanf("%d",&op);
if(op==1)
{
scanf("%d%d",&x,&y);
if(val(x)==-1||val(y)==-1)continue;
int fx=find(x),fy=find(y);
if(fx^fy)
f(fx)=f(fy)=merge(fx,fy);
}
if(op==2)
{
scanf("%d",&x);
if(val(x)==-1)
{
printf("-1\n");
continue;
}
int fx=find(x);
printf("%d\n",val(fx));
remove(fx);
}
}
}
int main(void)
{
input();
return 0;
}

Monkey King(洛谷P1456)

Description

Once in a forest, there lived N aggressive monkeys. At the beginning, they each does things in its own way and none of them knows each other. But monkeys can't avoid quarrelling, and it only happens between two monkeys who does not know each other. And when it happens, both the two monkeys will invite the strongest friend of them, and duel. Of course, after the duel, the two monkeys and all of there friends knows each other, and the quarrel above will no longer happens between these monkeys even if they have ever conflicted.

Assume that every money has a strongness value, which will be reduced to only half of the original after a duel(that is, 10 will be reduced to 5 and 5 will be reduced to 2).

And we also assume that every monkey knows himself. That is, when he is the strongest one in all of his friends, he himself will go to duel.

Input Format

There are several test cases, and each case consists of two parts.

First part: The first line contains an integer N(N<=100,000), which indicates the number of monkeys. And then N lines follows. There is one number on each line, indicating the strongness value of ith monkey(<=32768).

Second part: The first line contains an integer M(M<=100,000), which indicates there are M conflicts happened. And then M lines follows, each line of which contains two integers x and y, indicating that there is a conflict between the Xth monkey and Yth.

Output Format

For each of the conflict, output -1 if the two monkeys know each other, otherwise output the strength value of the strongest monkey among all of its friends after the duel.

Sample Input

5
20
16
10
10
4
5
2 3
3 4
3 5
4 5
1 5

Sample Output

8
5
5
-1
10

解析

很显然,我们需要一个支持最值查询,集合合并,单点修改的数据结构,这个就是左偏树的简单运用了。

维护左偏树森林,对于两个猴子打架,直接取出两棵左偏树中的根节点,并将其权值减半即可。但是,经过修改,左偏树就不一定具有堆性质了,我们还学要把根节点先删除,再合并回原树中,才能保证堆性质。

完成操作后,再合并两棵左偏树即可。

\(Code:\)

#include<bits/stdc++.h>
using namespace std;
#define mset(name,val) memset(name,val,sizeof name)
#define filein(str) freopen(str".in","r",stdin)
#define fileout(str) freopen(str".out","w",stdout)
const int N=100000+20,M=100000+20;
struct LeftistTree
{
int val,dis,l,r,f;
#define val(x) tree[x].val
#define dis(x) tree[x].dis
#define l(x) tree[x].l
#define r(x) tree[x].r
#define f(x) tree[x].f
}tree[N];
int n,m;
inline int find(int x){return f(x)==x?x:f(x)=find(f(x));}
inline int merge(int x,int y)
{
if(!x|!y)return x|y;
if( val(x)<val(y) || (val(x)==val(y)&&x>y) )
swap(x,y);
r(x)=merge( r(x) , y );
f( r(x) )=x;
if( dis( l(x) ) < dis( r(x) ) )
swap( l(x) , r(x) );
dis(x)=dis( r(x) )+1;
return x;
}
inline void input(void)
{
for(int i=1;i<=n;i++)
scanf("%d",&val(i)),f(i)=i;
scanf("%d",&m);
dis(0)=-1;
}
inline void solve(void)
{
for(int i=1;i<=m;i++)
{
int x,y,root,rootx,rooty;
scanf("%d%d",&x,&y);
int fx=find(x),fy=find(y);
if(fx==fy)
{
printf("-1\n");
continue;
}
val(fx) /= 2;
root = merge( l(fx) , r(fx) );
l(fx) = r(fx) = 0;
rootx = f(root) = f(fx) = merge( root , fx ); val(fy) /= 2;
root = merge( l(fy) , r(fy) );
l(fy) = r(fy) = 0;
rooty = f(root) = f(fy) = merge( root , fy ); root=merge( rootx , rooty );
printf("%d\n",val( find(root) ));
}
}
int main(void)
{
while(~scanf("%d",&n))
{
input();
solve();
mset(tree,0);
}
return 0;
}

<后记>

『左偏树 Leftist Tree』的更多相关文章

  1. 【BZOJ 1367】 1367: [Baltic2004]sequence (可并堆-左偏树)

    1367: [Baltic2004]sequence Description Input Output 一个整数R Sample Input 7 9 4 8 20 14 15 18 Sample Ou ...

  2. [note]左偏树(可并堆)

    左偏树(可并堆)https://www.luogu.org/problemnew/show/P3377 题目描述 一开始有N个小根堆,每个堆包含且仅包含一个数.接下来需要支持两种操作: 操作1: 1 ...

  3. Monkey King(左偏树 可并堆)

    我们知道如果要我们给一个序列排序,按照某种大小顺序关系,我们很容易想到优先队列,的确很方便,但是优先队列也有解决不了的问题,当题目要求你把两个优先队列合并的时候,这就实现不了了 优先队列只有插入 删除 ...

  4. 左偏树 / 非旋转treap学习笔记

    背景 非旋转treap真的好久没有用过了... 左偏树由于之前学的时候没有写学习笔记, 学得也并不牢固. 所以打算写这么一篇学习笔记, 讲讲左偏树和非旋转treap. 左偏树 定义 左偏树(Lefti ...

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

    左偏树是一种常用的优先队列(堆)结构.与二叉堆相比,左偏树可以高效的实现两个堆的合并操作. 左偏树实现方便,编程复杂度低,而且有着不俗的效率表现. 它的一个常见应用就是与并查集结合使用.利用并查集确定 ...

  6. 浅谈左偏树在OI中的应用

    Preface 可并堆,一个听起来很NB的数据结构,实际上比一般的堆就多了一个合并的操作. 考虑一般的堆合并时,当我们合并时只能暴力把一个堆里的元素一个一个插入另一个堆里,这样复杂度将达到\(\log ...

  7. BZOJ2333 [SCOI2011]棘手的操作 堆 左偏树 可并堆

    欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ2333 题意概括 有N个节点,标号从1到N,这N个节点一开始相互不连通.第i个节点的初始权值为a[i ...

  8. 【左偏树】【P3261】 [JLOI2015]城池攻占

    Description 小铭铭最近获得了一副新的桌游,游戏中需要用 m 个骑士攻占 n 个城池.这 n 个城池用 1 到 n 的整数表示.除 1 号城池外,城池 i 会受到另一座城池 fi 的管辖,其 ...

  9. 洛谷P3273 [SCOI2011] 棘手的操作 [左偏树]

    题目传送门 棘手的操作 题目描述 有N个节点,标号从1到N,这N个节点一开始相互不连通.第i个节点的初始权值为a[i],接下来有如下一些操作: U x y: 加一条边,连接第x个节点和第y个节点 A1 ...

随机推荐

  1. redis对键进行的相关操作

    redis对键操作的相关命令以及如何在python使用这些命令 redis对键操作的命令: 命令 语法 概述 返回值 Redis DEL 命令 del key [key ...] 该命令用于在 key ...

  2. 关于使用spring版本4.1.6注解@Import报错

    记录一下遇到的错误 org.springframework.beans.factory.parsing.BeanDefinitionParsingException: 使用环境:spring 4.1. ...

  3. functools.wraps函数

    原文地址:https://www.cnblogs.com/fcyworld/p/6239951.html 第一次见到functools.wraps是在 Flask Web开发 中,一直不明白怎么回事. ...

  4. vijos搭建踩坑

    nodejs我用的8.x版本,可以工作. 和制作组交谈之后他们说最好榨汁机和主机不要在同一系统下. vj4/vj4/handler/base.py的第343行 从 super(Connection, ...

  5. javascript函数传值问题(传值?址)

    通常对于我们开发者来说,有不少人是忽略了这些小问题的,但是我们又必要去了解.因为今天一个朋友问起,所以写到这里来了, 在C#中,我们知道如果要往一个函数中传递参数的类型为对象,数组或者其他引用类型时. ...

  6. Windows下编译jcef

    依赖软件参考 本文参考官方网站上的jcef编译过程 编译成功的环境如下: windows 10 64 bit JDK 1.8.0_121 64 bit Python 2.7.13 git versio ...

  7. h5适配的解决方案

    一. 流程 设计师以750pt×1334pt尺寸进行设计(当然高度随内容变化),最后用该尺寸的设计稿进行标注.切图,前端采用淘宝的开源方案flexible进行适配. 二. flexible使用方法 F ...

  8. Centos服务器上NFS灾备环境及KVM的搭建及使用

    1.概述 由于在单台服务器上搭建灾备环境需要KVM和NFS的支持,下面先列出KVM的搭建流程,再列出使用NFS实现单台服务器灾备的流程. A.搭建KVM环境 1>.主机环境准备 Linux Sy ...

  9. 自己封装element-ui树组件的过滤

    前言:vue开发项目时用到了element-ui的树组件,但是发现一执行过滤事件,树就全部都展开了,为了解决这个问题,只能自己先过滤数剧,再赋值给树组件的data,就避免了一上来全部展开的尴尬. 一. ...

  10. win10 anaconda安装后使用报错“Original error was: DLL load failed: 找不到指定的模块”

    报错:Original error was: DLL load failed: 找不到指定的模块. 环境变量需要添加3个 然后就okay了.