在了解可并堆之前,我们先复习一下堆的相关知识。堆是一种数据结构,支持插入元素,查询最大值(大根堆),查询最小值(小根堆),删除最大值最小值,稍微改动以后也支持删除任意一个元素,但由于手动实现这样的二叉堆较为复杂,通常情况下我们用系统自带的PQ(priority_queue)来实现普通堆。
那么可并堆是什么呢?顾名思义,可并堆就是支持合并操作(merge)的一种堆。容易得知,当堆支持合并操作以后,插入元素和删除操作也就会变得十分简单。插入操作,把欲插入的元素当成是一个堆,那么插入操作便转化成了堆的合并操作。删除操作,只需要把根(root)与其左儿子右儿子的边断掉,然后把以左儿子和右儿子为根的两个堆合并,那么我们便从逻辑上把根节点的元素删除了。下面重点来讲解可并堆的实现:
左偏树是可并堆最好用也是最好写的实现方式。定义一个点的dis值为:从这个点一直沿着右儿子走,最多能够走多少步。记dis[0]=-1。那么dis[x]=dis[rson]+1。那左偏树的定义是什么呢?对于树上的任意一个点,均有 dis[lson]≥dis[rson] ,这样的树即为一棵左偏树。左偏树满足两个性质:
1.左偏树的任意一棵子树都是左偏树。
2.对于子数大小为n的点,它的dis值不超过log n。(也是可并堆时间复杂度的保证)
用左偏树来实现合并操作便简化了许多。
左偏树的合并是一个递归过程,对于以 x 和 y 为根的树:
如果 x 和 y 二者之一为空树,则返回另一棵。
如果 x 和 y 均不为空,则比较 x 和 y 的权值大小。如果 x 的权值较大,那么就把 x 右儿子和 y 合并的结果作为 x 新的右儿子,返回 x ;否则就把x和y交换,继续上面的操作。合并以后由于我们还要维护左偏性质,即:如果合并后右儿子的dis大于左儿子的dis,则交换左右儿子。这便是通过左偏树来实现可并堆的模拟过程。
代码实现:
int merge(int x,int y) { if(!x) return y; if(!y) return x; if(v[x]<v[y]) swap(x,y); r[x]=merge(r[x],y); if(d[r[x]]>d[l[x]]) swap(l[x],r[x]); d[x]=d[r[x]]+1; return x; }
其中v[]表示权值,dis[]定义与上文所述相同,l[]左儿子,r[]右儿子。
这样我们便实现了堆的合并操作。不难发现,掌握可并堆之后,普通堆便没有了存在的意义。可并堆的时间复杂度比较神奇,合并操作复杂度O(dis[x]+dix[y]),可近似看成O(log n),但不能说可并堆时间复杂度为O(log n),其最坏情况下可以退化到O(n)。