<更新提示>

<第一次更新>

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


<正文>

左偏树 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:\)

  1. struct LeftistTree
  2. {
  3. int dis,val,l,r,f;
  4. #define dis(x) (tree[x].dis)
  5. #define val(x) (tree[x].val)
  6. #define l(x) (tree[x].l)
  7. #define r(x) (tree[x].r)
  8. #define f(x) (tree[x].f)
  9. }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:\)

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

取出堆顶 (find)

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

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

\(Code:\)

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

删除堆顶元素 (remove)

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

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

\(Code:\)

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

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

左偏树

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

  1. 5 5
  2. 1 5 4 2 3
  3. 1 1 5
  4. 1 2 5
  5. 2 2
  6. 1 4 2
  7. 2 2

Sample Output

  1. 1
  2. 2

\(Code:\)

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define filein(str) freopen(str".in","r",stdin)
  4. #define fileout(str) freopen(str".out","w",stdout)
  5. const int N=100000+20;
  6. struct LeftistTree
  7. {
  8. int dis,val,l,r,f;
  9. #define dis(x) (tree[x].dis)
  10. #define val(x) (tree[x].val)
  11. #define l(x) (tree[x].l)
  12. #define r(x) (tree[x].r)
  13. #define f(x) (tree[x].f)
  14. }tree[N];
  15. int n,m;
  16. inline int find(int x)
  17. {
  18. return f(x)==x?x:f(x)=find(f(x));
  19. }
  20. inline int merge(int x,int y)
  21. {
  22. if(!x||!y)return x|y;
  23. if(val(x)>val(y)||(val(x)==val(y)&&x>y))
  24. swap(x,y);
  25. r(x)=merge(r(x),y);
  26. f(r(x))=x;
  27. if(dis(l(x))<dis(r(x)))
  28. swap(l(x),r(x));
  29. dis(x)=dis(r(x))+1;
  30. return x;
  31. }
  32. inline void remove(int x)
  33. {
  34. val(x)=-1;
  35. f(l(x))=l(x);f(r(x))=r(x);
  36. f(x)=merge(l(x),r(x));
  37. }
  38. inline void input(void)
  39. {
  40. scanf("%d%d",&n,&m);
  41. for(int i=1;i<=n;i++)
  42. {
  43. f(i)=i;
  44. scanf("%d",&val(i));
  45. }
  46. int op,x,y;dis(0)=-1;
  47. for(int i=1;i<=m;i++)
  48. {
  49. scanf("%d",&op);
  50. if(op==1)
  51. {
  52. scanf("%d%d",&x,&y);
  53. if(val(x)==-1||val(y)==-1)continue;
  54. int fx=find(x),fy=find(y);
  55. if(fx^fy)
  56. f(fx)=f(fy)=merge(fx,fy);
  57. }
  58. if(op==2)
  59. {
  60. scanf("%d",&x);
  61. if(val(x)==-1)
  62. {
  63. printf("-1\n");
  64. continue;
  65. }
  66. int fx=find(x);
  67. printf("%d\n",val(fx));
  68. remove(fx);
  69. }
  70. }
  71. }
  72. int main(void)
  73. {
  74. input();
  75. return 0;
  76. }

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

  1. 5
  2. 20
  3. 16
  4. 10
  5. 10
  6. 4
  7. 5
  8. 2 3
  9. 3 4
  10. 3 5
  11. 4 5
  12. 1 5

Sample Output

  1. 8
  2. 5
  3. 5
  4. -1
  5. 10

解析

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

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

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

\(Code:\)

  1. #include<bits/stdc++.h>
  2. using namespace std;
  3. #define mset(name,val) memset(name,val,sizeof name)
  4. #define filein(str) freopen(str".in","r",stdin)
  5. #define fileout(str) freopen(str".out","w",stdout)
  6. const int N=100000+20,M=100000+20;
  7. struct LeftistTree
  8. {
  9. int val,dis,l,r,f;
  10. #define val(x) tree[x].val
  11. #define dis(x) tree[x].dis
  12. #define l(x) tree[x].l
  13. #define r(x) tree[x].r
  14. #define f(x) tree[x].f
  15. }tree[N];
  16. int n,m;
  17. inline int find(int x){return f(x)==x?x:f(x)=find(f(x));}
  18. inline int merge(int x,int y)
  19. {
  20. if(!x|!y)return x|y;
  21. if( val(x)<val(y) || (val(x)==val(y)&&x>y) )
  22. swap(x,y);
  23. r(x)=merge( r(x) , y );
  24. f( r(x) )=x;
  25. if( dis( l(x) ) < dis( r(x) ) )
  26. swap( l(x) , r(x) );
  27. dis(x)=dis( r(x) )+1;
  28. return x;
  29. }
  30. inline void input(void)
  31. {
  32. for(int i=1;i<=n;i++)
  33. scanf("%d",&val(i)),f(i)=i;
  34. scanf("%d",&m);
  35. dis(0)=-1;
  36. }
  37. inline void solve(void)
  38. {
  39. for(int i=1;i<=m;i++)
  40. {
  41. int x,y,root,rootx,rooty;
  42. scanf("%d%d",&x,&y);
  43. int fx=find(x),fy=find(y);
  44. if(fx==fy)
  45. {
  46. printf("-1\n");
  47. continue;
  48. }
  49. val(fx) /= 2;
  50. root = merge( l(fx) , r(fx) );
  51. l(fx) = r(fx) = 0;
  52. rootx = f(root) = f(fx) = merge( root , fx );
  53. val(fy) /= 2;
  54. root = merge( l(fy) , r(fy) );
  55. l(fy) = r(fy) = 0;
  56. rooty = f(root) = f(fy) = merge( root , fy );
  57. root=merge( rootx , rooty );
  58. printf("%d\n",val( find(root) ));
  59. }
  60. }
  61. int main(void)
  62. {
  63. while(~scanf("%d",&n))
  64. {
  65. input();
  66. solve();
  67. mset(tree,0);
  68. }
  69. return 0;
  70. }

<后记>

『左偏树 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. BP神经网络综合评价法

    BP神经网络综合评价法是一种交互式的评价方法,一种既能避免人为计取权重的不精确性, 又能避免相关系数求解的复杂性,还能对数量较大且指标更多的实例进行综合评价的方法,它可以根据用户期望的输出不断修改指标 ...

  2. 003 css总结

    1.题目 有哪项方式可以对一个DOM设置它的CSS样式? CSS都有哪些选择器? CSS选择器的优先级是怎么样定义的? CSS中可以通过哪些属性定义,使得一个DOM元素不显示在浏览器可视范围内? 超链 ...

  3. vue-cli3.0安装element-ui组件及按需引入element-ui组件

    在VUE-CLI 3下的第一个Element-ui项目(菜鸟专用) (https://www.cnblogs.com/xzqyun/p/10780659.html) 上面这个链接是vue-cli3.0 ...

  4. webpack4 打包报错 :regeneratorRuntime is not defined

    使用async函数,在webpack打包时报错 babel-polyfill is required. You must also install it in order to get async/a ...

  5. hadoop fs -put 报错

    [hadoop@master ~]$ ll total -rw-rw-r-- hadoop hadoop Apr : aaa drwxr-xr-x hadoop hadoop Jun Desktop ...

  6. 初次部署django+gunicorn+nginx

    初次部署django+gunicorn+nginx  博客详细地址  https://www.cnblogs.com/nanrou/p/7026802.html 写在前面,这只是我所遇到的情况,如果有 ...

  7. TimesTen数据库表中显示中文乱码的真正原因

    上一篇博客TimesTen中文乱码问题(其实是cmd.exe中文乱码)的内容可能不对,也许只是个巧合?不得而知了.因为我今天重装系统了,把win10换成了win7(64bit).又安装了timeste ...

  8. android BLE Peripheral 模拟 ibeacon 发出ble 广播

    Android对外模模式(peripheral)的支持: 从Android 5.0+开始才支持. api level >= 21 所以5.0 之前设备,是不能向外发送广播的. Android中心 ...

  9. modbus tcp数据报文结构

    modbus tcp数据报文结构 请求:00 00 00 00 00 06 09 03 00 00 00 01 响应:00 00 00 00 00 05 09 03 02 12 34 一次modbus ...

  10. 181102 Windows下安装kivy(用python写APP)

    了解到Instgram,知乎等APP是用python写的.我也决定学习用python写APP.这里我们需要安装kivy. 环境:win7,python3.6 安装方式:DOS命令窗口 注意事项:目前不 ...