左偏树

作为可并堆的一种,左偏树算是又好写功能全且复杂度比较优的了

首先介绍一下结构:
左偏是指定义的 dis 值左子树比右子树大
dis 指的是 min(son0,son1)+1,叶节点为零
注意这里的 dis 并不是深度,左偏树的深度是没有保证的,哪怕是一条链,只要满足左偏的性质就是符合的
所以要查询堆顶并不能暴力跳父亲,而是要额外开并查集来储存
能保证复杂度大概是因为只要有右子树是空的那么就能插入,那么 dis 的定义方式就很科学

那么接下来是变形操作
由于左偏树有着二叉树结构,那么也是支持懒标记的,一定注意每次删除堆顶前要先 spread
当然,理论上也可以可持久化,然而并没有遇见过这样的毒瘤题

以下是一些例题:


用于简单维护集合元素

P3261 [JLOI2015]城池攻占

堆中维护当前活着的骑士集合,由于骑士的死是一次性的且单调,那么每个点暴力弹出即可
每个城有增益效果,懒标记维护即可

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=3e5+5;
const int maxm=3e5+5;
int n,m,val[maxn],fa[maxn],son[maxn][2],dis[maxn],lazy[maxn],lazy1[maxn];
int sum[maxn],ans[maxn],dep[maxn],hd[maxn],cnt,rt[maxn],a[maxn],b[maxn],c[maxn],st[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;
}
struct Edge{
	int nxt,to;
}edge[maxm];
void add(int u,int v){
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;
}
void dospread(int x,int mul,int add){
	if(!x)return ;
	val[x]*=mul;val[x]+=add;lazy[x]*=mul;lazy1[x]*=mul;lazy1[x]+=add;
	return ;
}
void spread(int x){
	dospread(son[x][0],lazy[x],lazy1[x]);
	dospread(son[x][1],lazy[x],lazy1[x]);
	lazy[x]=1,lazy1[x]=0;
	return ;
}
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(val[x]>val[y])swap(x,y);
	spread(x);son[x][1]=merge(son[x][1],y);
	if(dis[son[x][0]]<dis[son[x][1]])swap(son[x][0],son[x][1]);
	dis[x]=dis[son[x][1]]+1;
	
	return x;
}
void dfs(int u){
	for(int i=hd[u];i;i=edge[i].nxt){
		int v=edge[i].to;dep[v]=dep[u]+1;
		dfs(v);rt[u]=merge(rt[u],rt[v]);
	}
	while(rt[u]&&val[rt[u]]<a[u]){
		ans[rt[u]]=dep[st[rt[u]]]-dep[u];sum[u]++;spread(rt[u]);
		rt[u]=merge(son[rt[u]][0],son[rt[u]][1]);
	}
	if(!b[u])dospread(rt[u],1,c[u]);
	else dospread(rt[u],c[u],0);
	return ;
}
signed main(){
	n=read(),m=read();dis[0]=-1;
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=2;i<=n;i++)fa[i]=read(),add(fa[i],i),b[i]=read(),c[i]=read();
	for(int i=1;i<=m;i++)val[i]=read(),st[i]=read(),lazy[i]=1,rt[st[i]]=merge(rt[st[i]],i);
	dfs(1);while(rt[1])ans[rt[1]]=dep[st[rt[1]]]+1,spread(rt[1]),rt[1]=merge(son[rt[1]][0],son[rt[1]][1]);
	for(int i=1;i<=n;i++)printf("%d\n",sum[i]);for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
	return 0;
}

用于优化 dp 转移

如果 dp 数组表示的是一些决策,而这些决策是具有贪心或单调性的,那么常常可以通过差分的方式优化
其实这个差分之所以可以优化,是因为相当于手动把所有决策拆分开来,因此对于较简单的问题常常直接从贪心+启发式合并的角度考虑会更好理解
对于 dp 数组是一个一次函数等特殊形式的情况,往往可以用一些技巧维护出这个函数的样子,而不用维护出每个位置


P7011 [CERC2013]Escape

f[u][i] 表示子树 ui 的血量进入能得到的血量
那么答案直接取 f[u][0] 即可,可以把 t 下面挂一个权值为 inf 的点
发现转移很浪费时间
那么不妨把 f 转化成多个二元组 (x,y),表示以 x 的血量进入最多增加的血量 y
过程变成了用当前血量对应从小到大取二元组直到不满足
那么每个点的二元组用堆来维护
那么每个点首先要把子树的堆合并起来
考虑加入 au 后的贡献:
如果 au 大于零,那么直接增加 (0,au)
否则考虑进行合并:首先以 au 作为初始血量来进入这个点,au 为初始的增加量
那么从小到大来取子树中的二元组来进行合并,直到不能合并为止
发现如果增加量为负一定不优,那么这时候需要继续拿,那么相应的血量应该变为 xy,指先消耗 y 还能进行拿取操作,直到增加为正为止

update:合并的部分貌似有点儿抽象啊,来补充一下
其实就是说如果 au 为负数,那么本来应该增加 (au,au),但是需要保证 x<au 的二元组不会被先取到,那么要把它们都消掉,也就是一直把它们和 (au,au) 合并
同时,为了避免加入一个负贡献的二元组,还需要继续往后合并一些二元组使得贡献为正,注意这样可能会让下限提高


CF671D Roads in Yusland

对于每个点的堆中存放所有覆盖了子树中所有边以及父亲边的方案,每次取出合法的堆顶即是最小值,设为 fx
对于一个节点合并子树时,设 sumfson,那么应该对子树 v 的所有方案加上 sumfv
同时对于每个堆顶排除不合法的方案

代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define pi pair<int,int>
#define fi first
#define se second
#define mp make_pair
const int maxn=3e5+5;
const int maxm=6e5+5;
int n,m,dis[maxn],lazy[maxn],dep[maxn],fa[maxn],f[maxn],son[maxn][2],rt[maxn],ans,x,y,w,hd[maxn],cnt;
pi val[maxn];
int read(){
	int x=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
	while(isdigit(ch)){x=x*10+ch-48;ch=getchar();}
	return x*f;	
}
struct Edge{
	int nxt,to;	
}edge[maxm];
void add(int u,int v){
	edge[++cnt].nxt=hd[u];
	edge[cnt].to=v;
	hd[u]=cnt;
	return ;	
}
void dospread(int x,int w){
	if(x)lazy[x]+=w,val[x].fi+=w;
	return ;	
}
void spread(int x){
	dospread(son[x][0],lazy[x]);
	dospread(son[x][1],lazy[x]);
	lazy[x]=0;
	return ;
}
int merge(int x,int y){
	if(!x||!y)return x+y;
	if(val[x]>val[y])swap(x,y);
	spread(x);son[x][1]=merge(son[x][1],y);
	if(dis[son[x][0]]<dis[son[x][1]])swap(son[x][0],son[x][1]);
	dis[x]=dis[son[x][1]]+1;
	return x;	
}
void dfs(int u){
	int sum=0;
	for(int i=hd[u];i;i=edge[i].nxt){
		int v=edge[i].to;
		if(v==fa[u])continue;
		fa[v]=u;dep[v]=dep[u]+1;dfs(v);
		dospread(rt[v],-f[v]);sum+=f[v];rt[u]=merge(rt[u],rt[v]);
	}
	dospread(rt[u],sum);
	while(rt[u]&&dep[val[rt[u]].se]>=dep[u]){
		spread(rt[u]);rt[u]=merge(son[rt[u]][0],son[rt[u]][1]);	
	}
	if(u==1)return ;
	if(!rt[u]){puts("-1");exit(0);}
	f[u]=val[rt[u]].fi;
	if(fa[u]==1)ans+=f[u];
	return ;
}
signed main(){
	n=read();m=read();dis[0]=-1;
	for(int i=1;i<=n-1;i++)x=read(),y=read(),add(x,y),add(y,x);
	for(int i=1;i<=m;i++)x=read(),y=read(),w=read(),val[i]=mp(w,y),rt[x]=merge(rt[x],i);
	dfs(1);cout<<ans;
	return 0;	
}

P4331 [BalticOI 2004]Sequence 数字序列

提供两种方法:

首先假设为简化版:b 的限制条件是小于等于
观察发现,对于一段递增的 a,那么 b 是对应的最优
对于一段递减的 a,那么 ba 的中位数时最优
那么考虑将原数列划分为许多段递减的段,每一段都取中位数
发现这样取完 b 后可能会出现前一个 b 比后一个 b 大的情况
那么把前一段和后一段的数合并后再求中位数即可
中位数可以用堆求,弹出直到剩下一半
由于有合并操作,用左偏树维护

这里提供一个更加精妙的做法:
考虑设计一个 dpf[i][j] 表示前 i 个最后一个为 j 的最小值
转移有两个:f[i][j]=f[i1][j]+|aij|,f[i][j]=min(f[i][j],f[i][j1])
如果对于每个 idp 数组看成一个关于 j 的函数图像
一定是一个下凸函数
加上取 min 操作,那么是一个先下降再不变的样子
那么考虑维护出每次这个函数加上一个下凸函数会变成什么样子
先画张图:

Screenshot 2021-11-26 at 14-47-43 图形计算器 - GeoGebra.png

考虑将所有的拐点都放进堆里
那么每次加入一个函数,会让顶点左边的斜率减 1,右边的加 1
那么最右边的一个拐点如果在顶点右侧,那么之后的斜率为零的直线会变成斜率为 1 的,然后取个 min 就没了,于是这种情况每次需要把堆顶弹出
每次更新 dp 数组所用的值一定是最后一个拐点,取堆顶即可


P3642 [APIO2016]烟火表演

题解


斜堆

这道题提到了斜堆的性质,顺便记录一下

P2475 [SCOI2008]斜堆

考虑最后一个节点的来源,根据题意,新插入的节点一定是一路向左,然后往下面挂左儿子得到的
那么只有两种情况:根节点左儿子路径上第一个没有右儿子的节点,或者是极左链的叶节点(且父亲没有右节点)
两种都判断一下,优先选第二种(因为字典序更小)

posted @   y_cx  阅读(96)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
历史上的今天:
2021-08-07 noip模拟32
点击右上角即可分享
微信分享提示