整理:可并堆
**# 关于左偏树的整理
1.左偏树是什么
左偏树是一种堆,支持 \(O(\log n)\) 地合并两个堆,插入一个新的元素,弹出堆顶元素,\(O(1)\) 查询堆顶元素。
左偏树满足一个性质,令 \(dist_u\) 表示以 \(u\) 为根的子树中,距离 \(u\) 最远的节点到 \(u\) 的距离,\(ls_u\) 和 \(rs_u\) 表示 \(u\) 的左儿子和右儿子,那么就有 \(dist_{ls_u}\ge dist_{rs_u}\)。
由于左偏树资瓷合并,所以左偏树又称可并堆。
2.左偏树的核心操作:merge
左偏树的 \(\operatorname{merge}\) 操作是左偏树的核心操作,左偏树的所有操作都是通过 \(\operatorname{merge}\) 操作实现的。
具体的,\(\operatorname{merge}\) 操作返回一个值,这里表示合并之后的堆顶元素编号,显然可以做一些特判,如果合并的两个堆有任意一个为空,那么返回另一个堆的堆顶编号即可,否则这里取对应值更小的编号作为合并之后的堆顶元素编号 \(u\)(如果为大根堆,就取对应值更大的编号作为合并之后的堆顶元素编号),目的是为了维护堆的性质,然后,我们合并 \(rs_u\) 代表的堆和另一个堆,递归下去,将返回的值作为 \(rs_u\),接着维护左偏性质,如果 \(dist_{rs_u}\) 比 \(dist_{ls_u}\) 大,那么交换 \(rs_u\) 和 \(ls_u\),将 \(dist_u\) 设为 \(dist_{ls_u}+1\)。
int merge(int u,int v){
if(u==0&&v==0)return 0;
if(u==0)return v;
if(v==0)return u;
if(val[u]>val[v])swap(u,v);
rs[u]=merge(rs[u],v);
if(dist[rs[u]]>dist[ls[u]])swap(rs[u],ls[u]);
dist[u]=dist[ls[u]]+1;
return u;
}
3.左偏树的其他操作
1.在一个堆中加入新元素
我们将要加入的元素视为一个新堆,将原来的堆和新堆合并即可。
int insert(int u,int v){
return merge(u,New(v));
}
2.删除一个堆的堆顶元素
直接合并 \(rs_u\) 和 \(ls_u\) 对应的堆。
int pop(int u){
return merge(ls[u],rs[u]);
}
3.将整个堆中的元素加上一个值
可以类比线段树做区间修改的时候打上的懒惰标记或者永久化标记。
对于懒惰标记,我们正常的打上标记,在访问儿子的时候下放即可。
void Tag(int p,int v){
val[p]+=v;
add[p]+=v;
//add[p] 记录 p 对应的堆整体加了多少
}
void pushdown(int p){
Tag(ls[p],add[p]);
Tag(rs[p],add[p]);
add[p]=0;
}
//更改之后的 merge 函数
int merge(int u,int v){
if(u==0&&v==0)return 0;
if(u==0)return v;
if(v==0)return u;
pushdown(u),pushdown(v);
if(val[u]>val[v])swap(u,v);
rs[u]=merge(rs[u],v);
if(dist[rs[u]]>dist[ls[u]])swap(rs[u],ls[u]);
dist[u]=dist[ls[u]]+1;
return u;
}
//更改之后的 pop 函数
int pop(int u){
pushdown(u);
return merge(ls[u],rs[u]);
}
对于永久化标记,在合并时可能需要一些其他的操作。
我们记录一个 \(siz_u\) 表示 \(u\) 对应的这个堆的大小,在合并时,我们进行一个类似启发式合并的操作,将大小更大的那个堆的标记作为合并之后的堆的标记,然后将更小的那个堆的标记直接下放到堆中即可。
可以得到这样的总时间复杂度为 \(O(n\log n)\)。
4.左偏树的使用
左偏树常用于树上问题,这些题目的有一个一般模式,也就是对所有节点各自维护一个堆,在 dfs 过程中和儿子的堆合并,然后弹出不合法的点,修改其中的节点,计算对应的答案即可。
比如 城池攻占。**

浙公网安备 33010602011771号