左偏树学习笔记

左偏树学习笔记

左偏树其实就是一个去实现可并堆这个数据结构的工具,其实完全可以用其他的写法,比如说配对堆,二叉堆,斐波那契堆等等,但是左偏树的写法更加常见,且使用起来更加方便,码量也不大。

我们一般可以用一个启发式合并实现 O(log2n) 的合并,但是这个左偏树可以实现 O(logn) 的合并。

对于一棵二叉树,我们定义 外节点 为子节点数小于两个的节点,定义一个节点的 d 为其到子树中最近的外节点所经过的边的数量。空节点的 d0

对于每一个点都是关于 d 左偏的,若在操作的时候不是左偏的,那就交换两节点。

这里来介绍一些左偏树(小根堆)的功能:

既然是可并堆的写法,那最重要的就是合并了。

这里合并是把两个点先比较一下值的大小,然后将较小值(u)的右端点设为右端点和 v 的合并后的根节点。

为什么是右端点呢,因为右边的深度较小,插到右边会使总体的深度增加较小。(实际上每次就至多增加 1 ).

然后维护左偏性质,返回较小值即可。

代码也十分精简:


int merge(int u, int v) {
	if (!u || !v)
		return u | v;
	if (val[u] > val[v])
		swap(u, v);
	rs[u] = merge(rs[u], v);
	if (d[rs[u]] > d[ls[u]])
		swap(rs[u], ls[u]);
	d[u] = d[rs[u]] + 1;
	return u;
}

那我们当然还需要一些辅助的数组,比如我们需要知道每一个点的根节点,这个可以使用一个并查集来写。

时间复杂度是 O(logn) 的。

那如果我们需要插入一个以前没有的节点呢,这个很好写,直接在结构体里建立一个 cnt 变量,来记录现在的节点数,每一次插入的时候直接 ++cnt 在和指定的节点 merge 即可。

如果需要实现删除操作,那就可以直接把两个儿子合并即可。

总体的模板代码:

struct LTREE {  //小根堆
	int ls[N],rs[N],d[N],cnt;
	int val[N];
	int merge(int u, int v) {
		if (!u || !v)return u | v;
		if (val[u] < val[v])swap(u, v);
		rs[u] = merge(rs[u], v);
		if (d[ls[u]] < d[rs[u]])swap(ls[u], rs[u]);
		d[u] = d[ls[u]] + 1;
		return u;
	}
	int insert(int x, int v) {
		++cnt;
		d[cnt] = 0, ls[cnt] = rs[cnt] = 0, val[cnt] = v;
		return merge(x, cnt);
	}
	int calc(int x) {
		return val[x];
	}
	int pop(int x) {
		return merge(ls[x], rs[x]);
	}
} lt;

但是其实还有个很大的问题,这个删除操作只能删除根节点的,一般节点删除其实也是可以的,但是作者懒得去写认为没有太大的必要,一般来说还是不太会写的。这里口胡一下,你需要每一次都像合并根节点的方式合并,然后直接跳到它的父亲节点,继续这样的操作即可,知道左右节点不需要交换即可。

这里看几道模板题。

P3377 【模板】左偏树/可并堆 - 洛谷 (luogu.com.cn)

这题直接套板子就可以了,记得有 -1 的判断。

#include<bits/stdc++.h>
using namespace std;
int flag[100005],id[100005],fa[100005];
int ls[100005], rs[100005], d[100005], cnt;
int  val[100005];
int merge(int u, int v) {
	if (!u || !v)
		return u | v;
	if (val[u] > val[v])
		swap(u, v);
	rs[u] = merge(rs[u], v);
	if (d[rs[u]] > d[ls[u]])
		swap(rs[u], ls[u]);
	d[u] = d[rs[u]] + 1;
	return u;
}
int insert(int x, int y) {
	cnt++;
	val[cnt] = y, ls[cnt] = rs[cnt] = d[cnt] = 0;
	return merge(cnt, x);
}
int top(int x) {
	return val[x];
}
int pop(int x) {
	return merge(ls[x], rs[x]);
}
int find(int x) {
	return fa[x]==x?x:fa[x]=find(fa[x]);
}
int n,m;
signed main() {
	scanf("%d%d",&n,&m);
	for(int i=1,x; i<=n; i++) {
		scanf("%d",&x);
		id[i]=insert(id[i],x);
		fa[i]=i;
	}
	for(int i=1,op,x,y; i<=m; i++) {
		scanf("%d%d",&op,&x);
		if(op==1) {
			scanf("%d",&y);
			if(flag[x]||flag[y])continue;
			x=find(x),y=find(y);
			if(x!=y)fa[x]=fa[y]=merge(x,y);
		} else {
			if(flag[x]){
				cout<<-1<<endl;
				continue;
			}
			x=find(x);
			cout<<top(x)<<endl;
			flag[x]=1;
			fa[x]=fa[ls[x]]=fa[rs[x]]=pop(x);
			ls[x]=0,rs[x]=0,d[x]=0;
		}
	}
	return 0;
}

P1456 Monkey King - 洛谷 (luogu.com.cn)

这题就是实现一个大根堆,每次取出堆头的元素,删除,再插入其值的一半,输出合并后的最大值即可。简单题。

[P3273 SCOI2011] 棘手的操作 - 洛谷 (luogu.com.cn)

这道题确实是一道棘手的题,比较烦的还是这个第 2 个操作和最后一个操作,第二个操作其实可以用单点修改的方法来写,只不过有点麻烦,而这个最后一个操作其实可以用一个 set 或者左偏树来维护,码量有点大。

[P4331 BalticOI 2004] Sequence 数字序列 - 洛谷 (luogu.com.cn)

这题其实主要是模型的构建,左偏树的部分还是不难的。

我们发现这个题要求{b} 是一个递增的序列,其实递增序列不太好写,而单调不减的序列会好写一点。

而有一个小技巧,把每一个数 ai 设为 aii, 同样的, bi 也设为 bii 这样我们就不用担心相邻项相等的问题了。

但是我们发现就算转化成了单调不减的形式,那还是不会做。

那不妨给自己设置一个部分分?

如果{a} 单调不减? 这个直接令 bi=ai 即可,答案为 0

那单调不增的情况呢?这里我们只要把 bi 设成 {a} 这个数组的中位数即可(思考为什么)。

那我们既然想到了这个的话,我们不妨把每一个单调不增的连续序列分为一块,因为如果是一个单调不减的数的话,每一个序列只有一个数,且中位数都是他本身。

但是这样就完了吗?当然不是,我们发现相邻的中位数可能不能保证单调不减,那怎么办?

这还不简单,直接把两个区间合并呀,但是其实就是把两个中位数合并即可,放在一个堆里。

对于每一个序列的单调性,可以用单调栈来维护,若现在的栈顶比栈顶下面那个数小的话就合并两个堆即可,这里的大小指的是中位数,合并之后一定要删到中位数为止,也就是实际区间大小比堆的大小大一倍才可以。

参考代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1E6+7;
int n,a[N],ans,top;
struct LTREE {  //大根堆
	int ls[N],rs[N],d[N],cnt;
	int val[N];
	int merge(int u, int v) {
		if (!u || !v)return u | v;
		if (val[u] < val[v])swap(u, v);
		rs[u] = merge(rs[u], v);
		if (d[ls[u]] < d[rs[u]])swap(ls[u], rs[u]);
		d[u] = d[ls[u]] + 1;
		return u;
	}
	int pop(int x) {
		return merge(ls[x], rs[x]);
	}
	int insert(int x, int v) {
		++cnt;
		d[cnt] = 0, ls[cnt] = rs[cnt] = 0, val[cnt] = v;
		return merge(x, cnt);
	}
} lt;
struct node{
	int l,r,rt,len,siz,val;
}st[N];
signed main() {
	scanf("%lld",&n);
	for(int i=1; i<=n; i++)
		scanf("%lld",a+i),a[i]-=i;
	for(int i=1; i<=n; i++) {
		st[++top]={i,i,i,1,1,a[i]};
		lt.insert(0,a[i]);
		while(top >1 && st[top-1].val > st[top].val ){//合并 
			top--;
			st[top].rt  =  lt.merge(st[top].rt ,st[top+1].rt);
			st[top].len += st[top+1].len;
			st[top].siz += st[top+1].siz;
			st[top].r   =  st[top+1].r ;
			while(st[top].siz > (st[top].len+1)/2 ){//取中位数 
				//cout<<st[top].val <<endl; 
				st[top].rt =lt.pop(st[top].rt);
				st[top].siz--;  
			}
			st[top].val=a[st[top].rt];
		}
		//cout<<st[1].len<<" "<<st[1].val <<endl;
	}
	for(int i=1;i<=top;i++)
		for(int j=st[i].l ;j<=st[i].r ;j++)
			ans+=abs(st[i].val -a[j]);
	cout<<ans<<endl;
	for(int i=1;i<=top;i++)
		for(int j=st[i].l ;j<=st[i].r ;j++)
			cout<<st[i].val +j<<" ";

	return 0;
}
posted @ 2025-02-19 14:28  hnczy  阅读(9)  评论(0编辑  收藏  举报