【左偏树】
左偏树是一种可并堆,也是一种优先队列容器。它除了支持查找最小值、删除最小值、插入值外,还支持优先队列不必需的合并(merge)操作。且左偏树的编程复杂度低,效率较高,适合竞赛中使用。
【左偏树的性质】
我们一般以存二叉树的方法来描述左偏树:对于每个节点,记录其左右儿子(无儿子的指向空节点0),这个节点的键值,以及节点的距离(距离的定义下面会讲)。
struct node{
int l,r,v,d;
void clear() {l=r=v=d=0;}
};
左偏树中,设点i的左右儿子分别为l,r则点i的距离被定义为l,r的距离中,较短距离+1,左右儿子中有空节点的节点称为外节点,特殊地,外节点的距离为0。另外,左偏树的任意子树都是一棵左偏树,所以我们用左偏树的根节点来代表一棵左偏树。
那么,左偏树满足以下两条性质:
1.堆性质:任意节点的优先度比其左右儿子高
2.左偏性质:任意节点(外节点除外)的左儿子距离一定不小于右儿子的距离。
显然可以得到推论:左偏树中任意节点i的距离为其右儿子距离+1
【左偏树的操作】
左偏树的合并操作是最重要的,因为删除最小值和插入值这两个操作都基于合并操作。合并操作merge(a,b)将a与b两棵左偏树合并,并返回合并后的左偏树。
合并操作的流程如下:
1.现有两棵左偏树a,b。显然,若其中一棵左偏树为空,则返回另一棵左偏树。
2.我们习惯把根节点键值较小的树作为被插入树,另一棵为插入树。不妨将a,b交换使其符合a为被插入树,b为插入树。
3.将b与a的右子树合并,合并后作为a的新右子树。
4.此时a有可能不满足左偏性质,只需交换a的左右儿子就可以使其重新满足该性质。
5.更新a的距离,返回a。
下面我们来探讨一下左偏树的左偏性质对其合并操作的复杂度有何影响。
上述过程中,第3步有较大疑问:为什么是b与右子树合并,而不是左子树?
我们回过头来思考:左偏树中的”距离”有何意义?距离的实质就是上述合并操作的递归层数!根据推论,点i的距离为其右儿子距离+1,那合并时每次与右儿子合并就能使时间复杂度降到最低!因为左偏树的距离都”偏向”左边,即右儿子的距离往往较小,意味着递归层数较少,从而降低了复杂度。
那么,之前的问题就迎刃而解。总之,最坏情况下,左偏树是很“平衡”的,两边距离相等,时间复杂度为O(logN)
下面给出代码:
int merge(int a,int b){
if (!a||!b) return a+b;
if (tre[a].v>tre[b].v) swap(a,b);
tre[a].r=merge(tre[a].r,b);
if (tre[tre[a].l].d<tre[tre[a].r].d) swap(tre[a].l,tre[a].r);
if (!tre[a].r) tre[a].d=0;else tre[a].d=tre[tre[a].r].d+1;
return a;
}
下面来讨论删除最小值的操作。
根据左偏树的特点,左偏树的根节点是不定的,需要另开变量。设当前要操作的树为Root,删除最小值的操作可以一句话完成:Root=merge(tre[Root].l,tre[Root].r);但是你会发现,Root这个节点的空间被释放后,得不到再次利用,这是一个极大的浪费(甚至可能导致溢出),所以我们需要开一个栈,来储存待回收的节点。
void pop(){
stk[++stk[0]]=Root;
Root=merge(tre[Root].l,tre[Root].r);
}
再来讨论如何插入值。
很简单,只需要把待插入的点看成单独的一棵左偏树,进行合并即可。
void psh(int x){
int now;
if (stk[0]) now=stk[stk[0]--];else now=(++len);
tre[now].clear();tre[now].v=x;
Root=merge(now,Root);
}
【优劣对比】
由上表可知,左偏树较之其他可并堆,在竞赛中还是比较实用的。