<更新提示>

<第一次更新>

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


<正文>

左偏树 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. Java中超大文件读写

    如果文件过大不能一次加载,就可以利用缓冲区: File file = new File(filepath); BufferedInputStream fis = new BufferedInputSt ...

  2. java并发编程可见性与线程封闭

    可见性 所谓可见性,指的是当一个线程修改了对象的状态后,其他线程能够看到该对象发生的变化.在单线程环境下,向某个变量写入值,然后在后面的操作再读取,在这个过程中该变量的值对该线程来说总是可见.但是,在 ...

  3. idea中自动生成实体类

    找到生成实体的路径,找到Database数据表 找到指定的路径即可自动生成entity实体 在创建好的实体类内如此修改 之后的步骤都在脑子里  写给自己看的东西 哪里不会就记录哪里 test类(以前都 ...

  4. 【web安全】-- springboot实现两次MD5加密

    一.为什么要做两次MD5 客户端MD5:HTTP在网络上是使用明文传输,用户输入的明文密码直接在网络上传输太危险.所以,在客户端先进行一次MD5(明文+固定盐). 服务端:服务端接受到后,也不是直接写 ...

  5. PC端问题列表及解决方案

    一.CSS相关 1.PC站百度文件引用不到,出现报错,问题可能是电脑拦截了百度广告. 解决方案:把拦截广告的浏览器插件关掉. 2.ie6双倍边距:在使用了float的情况下,不管是向左还是向右都会出现 ...

  6. VB读写进程的内存

    在窗体部分简单测试了ReadProcessMemory和WriteProcessMemory对另一个程序进程的读写. 由于临时项目变动,又不需要了,所以直接封类,删工程.以下代码没有一个函数经过测试, ...

  7. Shell 编程注意点

    (一)判断语句 [$# -lt 4 ]判断语句,格式[空格 比较对象1 比较符号 比较对象2] $# 启动脚本时携带参数个数;参数个数总数. $1 代表第一个参数. $? 最后一次执行名命令的退出状态 ...

  8. input标签实现小数点后两位保留小数

    短短一行代码就可以实现 <input type="number" min="0" max="100" step="0.01& ...

  9. Error: Unable to access xxx.jar

    在cmd中运行java -jar xxx.jar出现如下错误: Error: Unable to access xxx.jar 解决方法: 使用绝对路径:java -jar D:\Program Fi ...

  10. Openstack的视频学习

    0.安装环境准备 部署架构: 网络模式(红色Net0为管理网络,Net1接外网,Net2是接虚拟机网络流量的): 虚拟化平台为VirtualBox,虚拟网络Host-Only网络的配置: Net0:管 ...