『左偏树 Leftist Tree』
<更新提示>
<第一次更新>
<第二次更新>新增一道例题
<正文>
左偏树 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』的更多相关文章
- 【BZOJ 1367】 1367: [Baltic2004]sequence (可并堆-左偏树)
1367: [Baltic2004]sequence Description Input Output 一个整数R Sample Input 7 9 4 8 20 14 15 18 Sample Ou ...
- [note]左偏树(可并堆)
左偏树(可并堆)https://www.luogu.org/problemnew/show/P3377 题目描述 一开始有N个小根堆,每个堆包含且仅包含一个数.接下来需要支持两种操作: 操作1: 1 ...
- Monkey King(左偏树 可并堆)
我们知道如果要我们给一个序列排序,按照某种大小顺序关系,我们很容易想到优先队列,的确很方便,但是优先队列也有解决不了的问题,当题目要求你把两个优先队列合并的时候,这就实现不了了 优先队列只有插入 删除 ...
- 左偏树 / 非旋转treap学习笔记
背景 非旋转treap真的好久没有用过了... 左偏树由于之前学的时候没有写学习笔记, 学得也并不牢固. 所以打算写这么一篇学习笔记, 讲讲左偏树和非旋转treap. 左偏树 定义 左偏树(Lefti ...
- 左偏树(Leftist Heap/Tree)简介及代码
左偏树是一种常用的优先队列(堆)结构.与二叉堆相比,左偏树可以高效的实现两个堆的合并操作. 左偏树实现方便,编程复杂度低,而且有着不俗的效率表现. 它的一个常见应用就是与并查集结合使用.利用并查集确定 ...
- 浅谈左偏树在OI中的应用
Preface 可并堆,一个听起来很NB的数据结构,实际上比一般的堆就多了一个合并的操作. 考虑一般的堆合并时,当我们合并时只能暴力把一个堆里的元素一个一个插入另一个堆里,这样复杂度将达到\(\log ...
- BZOJ2333 [SCOI2011]棘手的操作 堆 左偏树 可并堆
欢迎访问~原文出处——博客园-zhouzhendong 去博客园看该题解 题目传送门 - BZOJ2333 题意概括 有N个节点,标号从1到N,这N个节点一开始相互不连通.第i个节点的初始权值为a[i ...
- 【左偏树】【P3261】 [JLOI2015]城池攻占
Description 小铭铭最近获得了一副新的桌游,游戏中需要用 m 个骑士攻占 n 个城池.这 n 个城池用 1 到 n 的整数表示.除 1 号城池外,城池 i 会受到另一座城池 fi 的管辖,其 ...
- 洛谷P3273 [SCOI2011] 棘手的操作 [左偏树]
题目传送门 棘手的操作 题目描述 有N个节点,标号从1到N,这N个节点一开始相互不连通.第i个节点的初始权值为a[i],接下来有如下一些操作: U x y: 加一条边,连接第x个节点和第y个节点 A1 ...
随机推荐
- 将source类中的属性值赋给target类中对应的属性
/** * 对象的属性值拷贝 * <p> * 将source对象中的属性值赋值到target对象中的属性,属性名一样,类型一样 * <p> * example: * <p ...
- java PDF分页打印
将获取的pdf文件按页拆分:参考https://q.cnblogs.com/q/99944/ pdf文件有多页,第一页需设置横向打印,其他页设置为纵向打印. PDDocument document = ...
- 使用Cordova打包Vue项目
因为公司项目要求, 原本的vue移动端项目, 现在要求能使用定位, 调用摄像头等功能, 并且开发成混合APP. 一个小白的孤军奋战史, 记录一下, 以备后用.... 第一步: 安装cordova 在命 ...
- python vs vscode问题汇总
最近在学工程目录章节的时候遭遇了个把 vscode目录管理 造成的问题, 当然第一大原因是: 初学者使用vscode的时候没有将环境设置好..... 闹了好几天的脾气, 这两天才整理好..... 这事 ...
- 结队第一次 plus
作业描述 作业所属课程:软件工程1916|W(福州大学) 作业要求:结对第一次-原型设计 结对学号:221600328 221600106 作业目标:尝试结对合作,使用NABCD模型,会分析用户需求, ...
- [SCOI2015]国旗计划
Description: A 国正在开展一项伟大的计划 -- 国旗计划.这项计划的内容是边防战士手举国旗环绕边境线奔袭一圈.这项计划需要多名边防战士以接力的形式共同完成,为此,国土安全局已经挑选了 \ ...
- js函数柯里化,实现bind
1.柯里化: 把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术. 举个栗子: 一个计算两数之和的函数,需要传递两个参数,柯里化 ...
- Tensor基本操作
Tensor(张量) 1.Tensor,又名张量,从工程角度来说,可简单地认为它就是一个数组,且支持高效的科学计算.它可以是一个数(标量).一维数组(向量).二维数组(矩阵)或更高维的数组(高阶数组) ...
- Redis sentinel 哨兵模式
一.sentinel介绍 Sentinel作用: 1):Master状态检测 2):如果Master异常,则会进行Master-Slave切换,将其中一个Slave作为Master,将之前的Maste ...
- linux操作命令之压缩命令
常用的压缩格式: .zip .gz .bz2 一..zip格式压缩 zip 压缩文件名 源文件 压缩文件 zip -r 压缩文件名 源目录 压缩目录 解压缩 unzip 压缩文件 ...