左偏树
左偏树是可合并的二叉堆,首先满足所有的堆的性质,其外,它还可以合并。
左偏树的树节点需要保存的信息有:
1.左右子树节点编号
2.此节点到有空子结点(子节点数不足2个的节点)的结点的最短距离dist
3.自身权值
性质
左偏树除了堆的所有性质,它还要满足的重要的性质就是“左偏”。
左偏
这个性质保证了它的操作都是O(logn)的。
左偏就是每个节点的左子节点的dist不小于右子节点的dist
(但并不代表左子节点数一定不小于右子节点数),
那么可知dist[i] =dist[rc[i]]+1;
如图(圈内是节点权值,蓝字就是dist值。)
操作
堆可以做到的是:插入(O(logn)),查询最值(O(1)),删除堆顶(O(logn));
对于左偏树,这些操作都是基于合并的(除了查询最值),而且复杂度都仍然是O(logn)。
左偏树合并操作合并的是两棵左偏树,对于堆的插入,就是合并一棵树和一个节点,对于堆的删除,就是合并根的两棵子树。
合并过程
以小根堆为例,
合并A, B两个堆
如果 A < B(这个不满足的话swap(a,b))and两棵树的节点没有包含关系(就是没有相同的节点)。
比较B和A的右子树大小,
如果B < A的右子树,那么swap B和A的右子树,
接着将B看成刚刚的A,继续swap;
如果B>A的右子树,那么继续找这颗树的右子树。
而这样可能会破坏左偏的性质,
所以需要在回溯的过程中维护左偏性质,
通过交换左右子树完成。
总的来说,左偏树的核心操作,合并(merge),是在右子树上进行的,
又因为要保证每个节点的左子节点的dist不小于右子节点的dist,
而且有dist[i] =dist[rc[i]]+1,
所以一棵左偏树的效率是O(logn)的
下面附上图会理解得更清晰:
// 左偏树模板, 以大根堆为例
struct node{
int w, lc, rc, h;
}t[M];//结构体
//核心操作 :合并(函数返回值:树根)
int merge(int A, int B){
if(!A||!B) return A+B;
if(t[A].w < t[B].w) swap(A, B);
t[A].rc = merge(t[A].rc, B);
maintain(A); //维护A的一些信息(此模板结构体中并没有),如:子节点数
if(t[t[A].lc].h < t[t[A].rc].h) swap(t[A].lc, t[A].rc);
if(t[A].rc) t[A].h = t[A].rc+1;
else t[A].h = 0;
return A;
}
//插入
void insert(int A, int x){
cnt++;
t[cnt].w = x;
merge(A, cnt);
}
//删除
void del(int A){
merge(t[A].lc, t[A].rc);
}