左偏树

基础定义

外节点:子节点数小于两个的节点。

一个节点的 distdist:其到子树中最近的外节点所经过的边的数量。空节点的 dist 为 00。


左偏树是一棵二叉树,它不仅具有堆的性质,并且是「左偏」的:每个节点左儿子的 distdist 都大于等于右儿子的 distdist。

注意这不是深度,所以左偏树的深度没有限制。


因此,distx=distrson+1dist_x=dist_{rson}+1。

操作

合并

以小根堆为例。

合并两个堆的时候,我们把根节点设成权值更小的点,然后将另一棵树和自己的右子树递归合并。最后如果不满足左偏性质就交换左右儿子。

至于时间复杂度,跟深度有关,而 distdist 是 log⁡\log 级别的,所以复杂度正确。

C++
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(t[x].v>t[y].v)swap(x,y);
	rs(x)=merge(rs(x),y);
	t[rs(x)].f=x;
	if(t[ls(x)].d<t[rs(x)].d)swap(ls(x),rs(x));
	t[x].d=t[rs(x)].d+1;
	return x;
}

查询

路径压缩可以查询到对顶,还是很简单的。

插入

就是普通堆的插入。

建堆

每次把队首的两个堆取出来,合并后扔到队尾,直到只剩下一个堆。

删除任意节点

先将左右儿子合并,然后自底向上更新 distdist、不满足左偏性质时交换左右儿子,当 distdist 无需更新时结束递归。

C++
void pushup(int x){
	if(!x)return;
	if(t[x].d!=t[rs(x)].d+1){
		t[x].d=t[rs(x)].d+1;
		pushup(t[x].f);
	}
}
void pop(int x,int y){
    int fa=t[y].f,root=merge(ls(y),rs(y));
    if(!fa){
        rt[x]=root;
        t[root].f=0;
        return;
    }
    if(ls(fa)==y)ls(fa)=root;
    else rs(fa)=root;
    t[root].f=fa;
    pushup(root);
    return;
}

我的错误

删除 xx 的时候,rtxrt_x 也需要赋值成为 merge(ls(x),rs(x)),因为有的点指向了它。

还有,如果两个点本来就在一个堆里面就不要合并了。

最 好 的写法

老朋友了,考场写起来是真的快。

C++
#include<bits/stdc++.h>
#include <ext/pb_ds/priority_queue.hpp>
using namespace std;
using namespace __gnu_pbds;
int n,m;
__gnu_pbds::priority_queue<int,greater<int>>q[1000010];
__gnu_pbds::priority_queue<int,greater<int>>::point_iterator p[1000010];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		int x;
		cin>>x;
		p[i]=q[i].push(x);
	}
	for(int i=1;i<=m;i++){
		int op,x,y,z;
		cin>>op;
		if(op==0){cin>>x>>y;q[x].erase(p[y]);};
		if(op==1){cin>>x;cout<<q[x].top()<<'\n';};
		if(op==2){cin>>x>>y;q[x].join(q[y]);};
		if(op==3){cin>>x>>y>>z;q[x].modify(p[y],z);};
	}
	return 0;
}

P1552 [APIO2012] 派遣

P1552 [APIO2012] 派遣。

这个就直接维护每个点薪水最小的那些人就行了,一点点合并上去。

P3642 [APIO2016] 烟花表演

P3642 [APIO2016] 烟花表演。

P4331 [BalticOI 2004] Sequence (Day1)

P4331 [BalticOI 2004] Sequence (Day1)。

将单调递增转化成单调不降的技巧:数组所有元素减去下标。

首先我们先找找这道题的性质。

我们发现,如果这个序列是单调递增的,我们只需要 zi=tiz_i=t_i,如果是单调递减,就取中位数就行了。

所以我们考虑能不能将序列拆成一段一段的。

如果这一段是单调递增的,就先不管,如果是单调递减的,我们考虑全选中位数,然后合并成一个集合。

然后相邻的集合如果左边的中位数小于右边的就不用管,否则合并两个集合的数来求新的中位数,成为新的集合。

重复这个过程就行了,中位数用可并堆求。

我们用可并堆,然后只需要保留 sz/2sz/2 个最小的节点,此时堆顶就是中位数。

因为我们保证了 vtop−1>vtopv_{top-1}>v_{top},所以新加入的所有元素只可能更小,所以其余大的那一部分可以直接删掉,不会影响答案。复杂度正确。

注意,我们需要用大根堆。

P3273 [SCOI2011] 棘手的操作

P3273 [SCOI2011] 棘手的操作。

题解还是太强了,pbds + 启发式合并堆竟然还是 O(log⁡n)O(\log n) 的。因为 pbds 的插入是 O(1)O(1) 的。

甚至 pbds 支持:for(int i:q[y])!

思路

这道题我们发现最难办的是对于块的合并时,块的整体 tagtag pbds 难以维护。

所以我们干脆就用优先队列(pbds O(1) insert),自己启发式合并就行了。

然后至于块 tagtag 对全局的贡献,我们只需要记录最大值的就行了,放进全局的堆中。然后还需要记录每个数的值,修改的时候直接修改,然后也需要放进全局的堆中。

其他操作就很简单了。