算法合集之《左偏树的特点及其应用》Word文件下载.docx
- 文档编号:18712886
- 上传时间:2022-12-31
- 格式:DOCX
- 页数:19
- 大小:94.07KB
算法合集之《左偏树的特点及其应用》Word文件下载.docx
《算法合集之《左偏树的特点及其应用》Word文件下载.docx》由会员分享,可在线阅读,更多相关《算法合集之《左偏树的特点及其应用》Word文件下载.docx(19页珍藏版)》请在冰豆网上搜索。
4.1例——数字序列(Baltic2004)13
五、左偏树与各种可并堆的比较15
5.1左偏树的变种——斜堆15
5.2左偏树与二叉堆的比较16
5.3左偏树与其他可并堆的比较16
六、总结18
【正文】
一、引言
优先队列在信息学竞赛中十分常见,在统计问题、最值问题、模拟问题和贪心问题等等类型的题目中,优先队列都有着广泛的应用。
二叉堆是一种常用的优先队列,它编程简单,效率高,但如果问题需要对两个优先队列进行合并,二叉堆的效率就无法令人满意了。
本文介绍的左偏树,可以很好地解决这类问题。
二、左偏树的定义和性质
在介绍左偏树之前,我们先来明确一下优先队列和可并堆的概念。
2.1优先队列,可并堆
2.1.1优先队列的定义
优先队列(PriorityQueue)是一种抽象数据类型(ADT),它是一种容器,里面有一些元素,这些元素也称为队列中的节点(node)。
优先队列的节点至少要包含一种性质:
有序性,也就是说任意两个节点可以比较大小。
为了具体起见我们假设这些节点中都包含一个键值(key),节点的大小通过比较它们的键值而定。
优先队列有三个基本的操作:
插入节点(Insert),取得最小节点(Minimum)和删除最小节点(Delete-Min)。
2.1.2可并堆的定义
可并堆(MergeableHeap)也是一种抽象数据类型,它除了支持优先队列的三个基本操作(Insert,Minimum,Delete-Min),还支持一个额外的操作——合并操作:
H←Merge(H1,H2)
Merge()构造并返回一个包含H1和H2所有元素的新堆H。
前面已经说过,如果我们不需要合并操作,则二叉堆是理想的选择。
可惜合并二叉堆的时间复杂度为O(n),用它来实现可并堆,则合并操作必然成为算法的瓶颈。
左偏树(LeftistTree)、二项堆(BinomialHeap)和Fibonacci堆(FibonacciHeap)都是十分优秀的可并堆。
本文讨论的是左偏树,在后面我们将看到各种可并堆的比较。
2.2左偏树的定义
左偏树(LeftistTree)是一种可并堆的实现。
左偏树是一棵二叉树,它的节点除了和二叉树的节点一样具有左右子树指针(left,right)外,还有两个属性:
键值和距离(dist)。
键值上面已经说过,是用于比较节点的大小。
距离则是如下定义的:
节点i称为外节点(externalnode),当且仅当节点i的左子树或右子树为空(left(i)=NULL或right(i)=NULL);
节点i的距离(dist(i))是节点i到它的后代中,最近的外节点所经过的边数。
特别的,如果节点i本身是外节点,则它的距离为0;
而空节点的距离规定为-1(dist(NULL)=-1)。
在本文中,有时也提到一棵左偏树的距离,这指的是该树根节点的距离。
左偏树满足下面两条基本性质:
[性质1]节点的键值小于或等于它的左右子节点的键值。
即key(i)≤key(parent(i))这条性质又叫堆性质。
符合该性质的树是堆有序的(Heap-Ordered)。
有了性质1,我们可以知道左偏树的根节点是整棵树的最小节点,于是我们可以在O
(1)的时间内完成取最小节点操作。
[性质2]节点的左子节点的距离不小于右子节点的距离。
即dist(left(i))≥dist(right(i))这条性质称为左偏性质。
性质2是为了使我们可以以更小的代价在优先队列的其它两个基本操作(插入节点、删除最小节点)进行后维持堆性质。
在后面我们就会看到它的作用。
这两条性质是对每一个节点而言的,因此可以简单地从中得出,左偏树的左右子树都是左偏树。
由这两条性质,我们可以得出左偏树的定义:
左偏树是具有左偏性质的堆有序二叉树。
下图是一棵左偏树:
2.3左偏树的性质
在前面一节中,本文已经介绍了左偏树的两个基本性质,下面本文将介绍左偏树的另外两个性质。
我们知道,一个节点必须经由它的子节点才能到达外节点。
由于性质2,一个节点的距离实际上就是这个节点一直沿它的右边到达一个外节点所经过的边数,也就是说,我们有
[性质3]节点的距离等于它的右子节点的距离加1。
即dist(i)=dist(right(i))+1外节点的距离为0,由于性质2,它的右子节点必为空节点。
为了满足性质3,故前面规定空节点的距离为-1。
我们的印象中,平衡树是具有非常小的深度的,这也意味着到达任何一个节点所经过的边数很少。
左偏树并不是为了快速访问所有的节点而设计的,它的目的是快速访问最小节点以及在对树修改后快速的恢复堆性质。
从图中我们可以看到它并不平衡,由于性质2的缘故,它的结构偏向左侧,不过距离的概念和树的深度并不同,左偏树并不意味着左子树的节点数或是深度一定大于右子树。
下面我们来讨论左偏树的距离和节点数的关系。
[引理1]若左偏树的距离为一定值,则节点数最少的左偏树是完全二叉树。
证明:
由性质2可知,当且仅当对于一棵左偏树中的每个节点i,都有dist(left(i))=dist(right(i))时,该左偏树的节点数最少。
显然具有这样性质的二叉树是完全二叉树。
[定理1]若一棵左偏树的距离为k,则这棵左偏树至少有2k+1-1个节点。
由引理1可知,当这样的左偏树节点数最少的时候,是一棵完全二叉树。
距离为k的完全二叉树高度也为k,节点数为2k+1-1,所以距离为k的左偏树至少有2k+1-1个节点。
作为定理1的推论,我们有:
[性质4]一棵N个节点的左偏树距离最多为log(N+1)-1。
设一棵N个节点的左偏树距离为k,由定理1可知,N≥2k+1-1,因此k≤log(N+1)-1。
有了上面的4个性质,我们可以开始讨论左偏树的操作了。
三、左偏树的操作
本章将讨论左偏树的各种操作,包括插入新节点、删除最小节点、合并左偏树、构建左偏树和删除任意节点。
由于各种操作都离不开合并操作,因此我们先讨论合并操作。
3.1左偏树的合并
C←Merge(A,B)
Merge()把A,B两棵左偏树合并,返回一棵新的左偏树C,包含A和B中的所有元素。
在本文中,一棵左偏树用它的根节点的指针表示。
在合并操作中,最简单的情况是其中一棵树为空(也就是,该树根节点指针为NULL)。
这时我们只须要返回另一棵树。
若A和B都非空,我们假设A的根节点小于等于B的根节点(否则交换A,B),把A的根节点作为新树C的根节点,剩下的事就是合并A的右子树right(A)和B了。
right(A)←Merge(right(A),B)
合并了right(A)和B之后,right(A)的距离可能会变大,当right(A)的距离大于left(A)的距离时,左偏树的性质2会被破坏。
在这种情况下,我们只须要交换left(A)和right(A)。
若dist(left(A))>
dist(right(A)),交换left(A)和right(A)
最后,由于right(A)的距离可能发生改变,我们必须更新A的距离:
dist(A)←dist(right(A))+1
不难验证,经这样合并后的树C符合性质1和性质2,因此是一棵左偏树。
至此左偏树的合并就完成了。
下图是一个合并过程的示例:
合并流程
我们可以用下面的代码描述左偏树的合并过程:
下面我们来分析合并操作的时间复杂度。
从上面的过程可以看出,每一次递归合并的开始,都需要分解其中一棵树,总是把分解出的右子树参加下一步的合并。
根据性质3,一棵树的距离决定于其右子树的距离,而右子树的距离在每次分解中递减,因此每棵树A或B被分解的次数分别不会超过它们各自的距离。
根据性质4,分解的次数不会超过log(N1+1)+log(N2+1)-2,其中N1和N2分别为左偏树A和B的节点个数。
因此合并操作最坏情况下的时间复杂度为O(log(N1+1)+log(N2+1)-2)=O(logN1+logN2)。
3.2插入新节点
单节点的树一定是左偏树,因此向左偏树插入一个节点可以看作是对两棵左偏树的合并。
下面是插入新节点的代码:
由于合并的其中一棵树只有一个节点,因此插入新节点操作的时间复杂度是O(logn)。
3.3删除最小节点
由性质1,我们知道,左偏树的根节点是最小节点。
在删除根节点后,剩下的两棵子树都是左偏树,需要把他们合并。
删除最小节点操作的代码也非常简单:
由于删除最小节点后只需进行一次合并,因此删除最小节点的时间复杂度也为O(logn)。
3.4左偏树的构建
将n个节点构建成一棵左偏树,这也是一个常用的操作。
算法一暴力算法——逐个节点插入,时间复杂度为O(nlogn)。
算法二仿照二叉堆的构建算法,我们可以得到下面这种算法:
Ø
将n个节点(每个节点作为一棵左偏树)放入先进先出队列。
不断地从队首取出两棵左偏树,将它们合并之后加入队尾。
当队列中只剩下一棵左偏树时,算法结束。
构建流程
下面分析算法二的时间复杂度。
假设n=2k,则:
前
次和并的是两棵只有1个节点的左偏树。
接下来的
次合并的是两棵有2个节点的左偏树。
次合并的是两棵有4个节点的左偏树。
……
次合并的是两棵有2i-1个节点的左偏树。
合并两棵2i个节点的左偏树时间复杂度为O(i),因此算法二的总时间复杂度为:
。
3.5删除任意已知节点
接下来是关于删除任意已知节点的操作。
之所以强调“已知”,是因为这里所说的任意节点并不是根据它的键值找出来的,左偏树本身除了可以迅速找到最小节点外,不能有效的搜索指定键值的节点。
故此,我们不能要求:
请删除所有键值为100的节点。
前面说过,优先队列是一种容器。
对于通常的容器来说,一旦节点被放进去以后,容器就完全拥有了这个节点,每个容器中的节点具有唯一的对象掌握它的拥有权(ownership)。
对于这种容器的应用,优先队列只能删除最小节点,因为你根本无从知道它的其它节点是什么。
但是优先队列除了作为一种容器外还有另一个作用,就是可以找到最小节点。
很多应用是针对这个功能的,它们并没有将拥有权完全转移给优先队列,而是把优先队列作为一个最小节点的选择器,从一堆节点中依次将它们选出来。
这样一来节点的拥有权就可能同时被其它对象掌握。
也就是说某个节点虽不是最小节点,不能从优先队列那里“已知”,但却可以从其它的拥有者那里“已知”。
这种优先队列的应用也是很常见的。
设想我们有一个闹钟,它可以记录很多个响铃时间,不过由于时间是线性的,铃只能一个个按先后次序响,优先队列就很适合用来作这样的挑选。
另一方面使用者应该可以随时取消一个“已知”的响铃时间,这就需要进行任意已知节点的删除操作了。
我们的这种删除操作需要指定被删除的节点,这和原来的删除根节点的操作是兼容的,因为根节点肯定是已知的。
上面已经提过,在删除一个节点以后,将会剩下它的两棵子树,它们都是左偏树,我们先把它们合并成一棵新的左偏树。
p←Merge(left(x),right(x))
现在p指向了这颗新的左偏树,如果我们删除的是根节点,此时任务已经完成了。
不过,如果被删除节点x不是根节点就有点麻烦了。
这时p指向的新树的距离有可能比原来x的距离要大或小,这势必有可能影响原来x的父节点q的距离,因为q现在成为新树p的父节点了。
于是就要仿照合并操作里面的做法,对q的左右子树作出调整,并更新q的距离。
这一过程引起了连锁反应,我们要顺着q的父节点链一直往上进行调整。
新树p的距离为dist(p),如果dist(p)+1等于q的原有距离dist(q),那么不管p是q的左子树还是右子树,我们都不需要对q进行任何调整,此时删除操作也就完成了。
如果dist(p)+1小于q的原有距离dist(q),那么q的距离必须调整为dist(p)+1,而且如果p是左子树的话,说明q的左子树距离比右子树小,必须交换子树。
由于q的距离减少了,所以q的父节点也要做出同样的处理。
剩下就是另外一种情况了,那就是p的距离增大了,使得dist(p)+1大于q的原有距离dist(q)。
在这种情况下,如果p是左子树,那么q的距离不会改变,此时删除操作也可以结束了。
如果p是右子树,这时有两种可能:
一种是p的距离仍小于等于q的左子树距离,这时我们直接调整q的距离就行了;
另一种是p的距离大于q的左子树距离,这时我们需要交换q的左右子树并调整q的距离,交换完了以后q的右子树是原来的左子树,它的距离加1只能等于或大于q的原有距离,如果等于成立,删除操作可以结束了,否则q的距离将增大,我们还要对q的父节点做出相同的处理。
删除任意已知节点操作的代码如下:
下面分两种情况讨论删除操作的时间复杂度。
情况1:
p的距离减小了。
在这种情况下,由于q的距离只能缩小,当循环结束时,要么根节点处理完了,q为空;
要么p是q的右子树并且dist(p)+1=dist(q);
如果dist(p)+1>
dist(q),那么p一定是q的左子树,否则会出现q的右子树距离缩小了,但是加1以后却大于q的距离的情况,不符合左偏树的性质3。
不论哪种情况,删除操作都可以结束了。
注意到,每一次循环,p的距离都会加1,而在循环体内,dist(p)+1最终将成为某个节点的距离。
根据性质4,任何的距离都不会超过logn,所以循环体的执行次数不会超过logn。
情况2:
p的距离增大了。
在这种情况下,我们将必然一直从右子树向上调整,直至q为空或p是q的左子树时停止。
一直从右子树升上来这个事实说明了循环的次数不会超过logn(性质4)。
最后我们看到这样一个事实,就是这两种情况只会发生其中一个。
如果某种情况的调整结束后,我们已经知道要么q为空,要么dist(p)+1=dist(q),要么p是q的左子树。
这三种情况都不会导致另一情况发生。
直观上来讲,如果合并后的新子树导致了父节点的一系列距离调整的话,要么就一直是往小调整,要么是一直往大调整,不会出现交替的情况。
我们已经知道合并出新子树p的复杂度是O(logn),向上调整距离的复杂度也是O(logn),故删除操作的最坏情况的时间复杂度是O(logn)。
如果左偏树非常倾斜,实际应用情况下要比这个快得多。
3.6小结
本章介绍了左偏树的各种操作,我们可以看到,左偏树作为可并堆的实现,它的各种操作性能都十分优秀,且编程复杂度比较低,可以说是一个“性价比”十分高的数据结构。
左偏树之所以是很好的可并堆实现,是因为它能够捕捉到具有堆性质的二叉树里面的一些其它有用信息,没有将这些信息浪费掉。
根据堆性质,我们知道,从根节点向下到任何一个外节点的路径都是有序的。
存在越长的路径,说明树的整体有序性越强,与平衡树不同(平衡树根本不允许有很长的路径),左偏树尽大约一半的可能保留了这个长度,并将它甩向左侧,利用它来缩短节点的距离以提高性能。
这里我们不进行严格的讨论,左偏树作为一个例子大致告诉我们:
放弃已有的信息意味着算法性能上的牺牲。
下面是最好的左偏树:
有序表(插入操作是按逆序发生的,自然的有序性被保留了)和最坏的左偏树:
平衡树(插入操作是按正序发生的,自然的有序性完全被放弃了)。
四、左偏树的应用
4.1例——数字序列(Baltic2004)
[问题描述]
给定一个整数序列a1,a2,…,an,求一个不下降序列b1≤b2≤…≤bn,使得数列{ai}和{bi}的各项之差的绝对值之和|a1-b1|+|a2-b2|+…+|an-bn|最小。
[数据规模]1≤n≤106,0≤ai≤2*109
[初步分析]
我们先来看看两个最特殊的情况:
1.a[1]≤a[2]≤…≤a[n],在这种情况下,显然最优解为b[i]=a[i];
2.a[1]≥a[2]≥…≥a[n],这时,最优解为b[i]=x,其中x是数列a的中位数。
于是我们可以初步建立起这样一个思路:
把1…n划分成m个区间:
[q[1],q[2]-1],[q[2],q[3]-1],……,[q[m],q[m+1]-1]。
每个区间对应一个解,b[q[i]]=b[q[i]+1]=…=b[q[i+1]-1]=w[i],其中w[i]为a[q[i]],a[q[i]+1],...,a[q[i+1]-1]的中位数。
显然,在上面第一种情况下m=n,q[i]=i;
在第二种情况下m=1,q[1]=1。
这样的想法究竟对不对呢?
应该怎样实现?
若某序列前半部分a[1],a[2],…,a[n]的最优解为(u,u,…,u),后半部分a[n+1],a[n+2],...,a[m]的最优解为(v,v,…,v),那么整个序列的最优解是什么呢?
若u≤v,显然整个序列的最优解为(u,u,…,u,v,v,…,v)。
否则,设整个序列的最优解为(b[1],b[2],…,b[m]),则显然b[n]≤u(否则我们把前半部分的解(b[1],b[2],…,b[n])改为(u,u,…,u),由题设知整个序列的解不会变坏),同理b[n+1]≥v。
接下来,我们将看到下面这个事实:
对于任意一个序列a[1],a[2],…,a[n],如果最优解为(u,u,…,u),那么在满足u≤u′≤b[1]或b[n]≤u′≤u的情况下,(b[1],b[2],…,b[n])不会比(u′,u′,…,u′)更优。
我们用归纳法证明u≤u′≤b[1]的情况,b[n]≤u′≤u的情况可以类似证明。
当n=1时,u=a[1],命题显然成立。
当n>
1时,假设对于任意长度小于n的序列命题都成立,现在证明对于长度为n的序列命题也成立。
首先把(b[1],b[2],…b[n])改为(b[1],b[1],…b[1]),这一改动将不会导致解变坏,因为如果解变坏了,由归纳假设可知a[2],a[3],…,a[n]的中位数w>
u,这样的话,最优解就应该为(u,u,…,u,w,w,…,w),矛盾。
然后我们再把(b[1],b[1],…,b[1])改为(u′,u′,…,u′),由于|a[1]-x|+|a[2]-x|+…+|a[n]-x|的几何意义为数轴上点x到点a[1],a[2],…a[n]的距离之和,且u≤u′≤b[1],显然点u′到各点的距离之和不会比点b[1]到各点的距离之和大,也就是说,(b[1],b[1],…,b[n])不会比(v,v,…,v)更优。
(证毕)
再回到之前的论述,由于b[n]≤u,作为上述事实的结论,我们可以得知,将(b[1],b[2],…,b[n])改为(b[n],b[n],…,b[n]),再将(b[n+1],b[n+2],…,b[m])改为(b[n+1],b[n+1],…,b[n+1]),并不会使解变坏。
也就是说,整个序列的最优解为(b[n],b[n],…,b[n],b[n+1],b[n+1],…,b[n+1])。
再考虑一下该解的几何意义,设整个序列的中位数为w,则显然令b[n]=b[n+1]=w将得到整个序列的最优解,即最优解为(w,w,…,w)。
分析到这里,我们一开始的想法已经有了理论依据,算法也不难构思了。
[算法描述]
延续我们一开始的思路,假设我们已经找到前k个数a[1],a[2],…,a[k](k<
n)的最优解,得到m个区间组成的队列,对应的解为(w[1],w[2],…,w[m]),现在要加入a[k+1],并求出前k+1个数的最优解。
首先我们把a[k+1]作为一个新区间直接加入队尾,令w[m+1]=a[k+1],然后不断检查队尾两个区间的解w[m]和w[m+1],如果w[m]>
w[m+1],我们需要将最后两个区间合并,并找出新区间的最优解(也就是序列a中,下标在这个新区间内的各项的中位数)。
重复这个合并过程,直至w[1]≤w[2]≤…≤w[m]时结束,然后继续处理下一个数。
这个算法的正确性前面已经论证过了,现在我们需要考虑一下数据结构的选取。
算法中涉及到以下两种操作:
合并两个有序集以及查询某个有序集内的中位数。
能较高效地支持这两种操作的数据结构有不少,一个比较明显的例子是二叉检索树(BST),它的询问操作复杂度是O(logn),但合并操作不甚理想,采用启发式合并,总时间复杂度为O(nlog2n)。
有没有更好的选择呢?
通过进一步分析,我们发现,只有当某一区间内的中位数比后一区间内的中位数大时,合并操作才会发生,也就是说,任一区间与后面的区间合并后,该区间内的中位数不会变大。
于是我们可以用最大堆来维护每个区间内的中位数,当堆中的元素大于该区间内元素的一半时,删除堆顶元素,这样堆中的元素始终为区间内较小的一半元素,堆顶元素即为该区间内的中位数。
考虑到我们必须高效地完成合并操作,左偏树是一个理想的选择。
左偏树的询问操作时间复杂度为O
(1),删除和合并操作时间复杂度都是O(logn),而询问操作和合并操作少于n次,删除操作不超过n/2次(因为删除操作只会在合并两个元素个数为奇数的堆时发生),因此用左偏树实现,可以把算法的时间复杂度降为O(nlogn)。
[小结]
这道题的解题过程对我们颇有启示。
在应用左偏树解题时,我们往往会觉得题目无从下手,甚至与左偏树毫无关系,但只要我们对题目深入分析,加以适当的转化,问题终究会迎刃而解。
这需要我们具有敏捷的思维以及良好的题感。
用左偏树解本题,相比较于前面BST的解法,时间复杂度和编程复杂度更低,这使我们不得不感叹于左偏树的神奇威力。
这不是说左偏树就一定是最好的解法,就本题来说,解法有很多种,光是可并堆的解法,就可以用多种数据结构来实现,但左偏树相对于它们,还是有一定的优势的,这将在下一章详细讨论。
五、左偏树与各种可并堆的比较
我们知道,左偏树是一种可并堆的实现。
但是为什么我们要用左偏树实现可并堆呢?
左偏树相对于其他可并堆有什么优点?
本章将就这个问题展开讨论,介绍各种可并堆的特点,并对它们做出比较。
5.1左偏树的变种——斜堆
这里我们要介绍左偏树的一个变种——斜堆(SkewHeap)。
斜堆是一棵堆有序的二叉树,但是它不满足左偏性质,或者说,斜堆根本就没有“距离”这个概念——它不需要记录任何一个节点的距离。
从结构上来说,所有的左偏树都是斜堆,但反之不然。
类似于左偏树,斜堆的各种操作也是在合并操作的基础上完成的,因此这里只介绍斜堆的合并操作,其他操作读者都可以仿照左偏树完成。
斜堆合并操作的递归合并过程和左偏树完
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- 左偏树的特点及其应用 算法 左偏树 特点 及其 应用
![提示](https://static.bdocx.com/images/bang_tan.gif)