左偏树/可并堆

简介

什么是左偏树?
上面的树都是左偏树。
先引出一个概念,dis等于节点到它子树里面最近的叶子节点的距离,特别地叶子节点的dis等于0。
观察上图我们可以感性理解左偏树,就是左子树的深度大于等于右子树,看上去整个树向左偏。
再看一眼就可以总结出几条性质:
1.左儿子的\(dis\)<=右儿子的\(dis\)(左偏性质)
2.节点的\(dis\)=右儿子的\(dis\)+1(因为存的是最近的叶子节点,右边的叶子一定离得更近)
3.每个节点的值一定小于等于儿子节点的值(堆性质)
不要问为什么,只有满足这些条件的才叫左偏树。
同时由上面的性质可以推出:任何时候,节点数为\(n\)的左偏树,距离最大为\(\log(n+1)−1\)
证明:
对于一棵\(dis=k\)的树,需要的最少的节点数是满二叉树(少一个点dis就等于k-1)。
若一棵左偏树的距离为k,则这棵左偏树至少有2k+1−1个点。

\[n>=2k+1-1 \]

\[n+1>=2k+1 \]

\[log(n+1)>=k+1 \]

\[log(n+1)-1>=k \]

因为上面这个性质可得左偏树的深度有限制,两个左偏树进行合并可以用\(Θ(logn)\)的复杂度。

基本操作

1.\(merge\)(合并)
两个左偏堆合并,要满足堆的性质,所以从两个根中选出小的一个当根,大的那个与右子树合并即可递归到只有一或两个节点的情况。
因为深度最大为\(log(n+1)\)所以一次复杂度\(Θ(logn)\)

inline int merge(int x,int y)
{
    if(!x||!y)return x+y;//边界情况
    if(dui[x].v>dui[y].v||(dui[x].v==dui[y].v&&x>y))swap(x,y);//使x为小的那个的根
    rs=merge(rs,y);//递归,将y与右子树合并
    if(dui[ls].dis<dui[rs].dis)swap(ls,rs);//堆建完后,保证左偏堆性质交换左右子树
    dui[ls].rt=dui[rs].rt=dui[x].rt=x;//更新根
    dui[x].dis=dui[rs].dis+1;//更新dis
    return x;
}

2.\(pop\)(删除x节点所在堆的最小值/最大值)

inline void pop(int x)
{
    dui[x].v=-1;
    dui[ls].rt=ls;dui[rs].rt=rs;//更新左右子树的根
    dui[x].rt=merge(ls,rs);//将左右子树合成新的堆,删除后保留原来的堆的根会有用
}

3.\(De\)l:(删除任意(x)编号节点)
\(x\)删掉,将\(x\)的左右儿子合并,然后接到\(f[x]\)的儿子处。
因为这时可能不满足节点的\(dis\)=右儿子的\(dis\)+1的性质。
所以向上更改。

void pushup(int x)
{
    if(x==f[x])return ;//达到根节点,返回
    if(t[x].d!=t[rs(x)].d+1)//不满足左偏性质,更新
    {
        t[x].d=t[rs(x)].d+1;
        pushup(f[x]);
    }
}
void del(int x)
{
    int fx=f[x];//x的父亲
    int u=merge(t[x].ch[0],t[x].ch[1]);//合并左右儿子
    f[u]=fx;//更新合并后的节点的信息
    if(t[fx].ch[0]==x)t[fx].ch[0]=u;
    else t[fx].ch[1]=u;
    t[x].val=t[x].ch[0]=t[x].ch[1]=t[x].d=0;
    pushup(x);//遍历检查左偏性质
}

4.\(Push\)(插入\(x\)节点)
新建一个节点,将其初始化为\(x\)
因为这个节点也可以视为一个堆,可以直接合并
5.并查集存堆找根并路径压缩

inline int get(int x)
{
    return dui[x].rt==x?x:dui[x].rt=get(dui[x].rt);
}

例题:

P3377 【模板】左偏树/可并堆

#include <bits/stdc++.h>
using namespace std;
#define ls dui[x].son[0]
#define rs dui[x].son[1]
int n,m; 
struct node
{
    int son[2],rt,v,dis;
}dui[100010];
inline int merge(int x,int y)
{
    if(!x||!y)return x+y;
    if(dui[x].v>dui[y].v||(dui[x].v==dui[y].v&&x>y))swap(x,y);
    rs=merge(rs,y);
    if(dui[ls].dis<dui[rs].dis)swap(ls,rs);
    dui[ls].rt=dui[rs].rt=dui[x].rt=x;
    dui[x].dis=dui[rs].dis+1;
    return x;
}
inline int get(int x)
{
    return dui[x].rt==x?x:dui[x].rt=get(dui[x].rt);
}
inline void pop(int x)
{
    dui[x].v=-1;
    dui[ls].rt=ls;dui[rs].rt=rs;
    dui[x].rt=merge(ls,rs);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&dui[i].v);
        dui[i].rt=i;
    }
    for(int i=1;i<=m;i++)
    {
        int op,x,y;
        scanf("%d%d",&op,&x);
        if(op==1)
        {
            scanf("%d",&y);
            if(dui[x].v==-1||dui[y].v==-1)continue;
            int f1=get(x),f2=get(y);
            if(f1==f2)continue;
            else merge(f1,f2);            
        }
        else
        {
            if(dui[x].v==-1)printf("-1\n");
            else printf("%d\n",dui[get(x)].v),pop(get(x));
        }
    }
    return 0;
} 

双倍经验

P1456 Monkey King

其他操作与上两道题没什么区别,主要是如何实现决斗后值除以 \(2\),显然可以暴力做,先将这个点删了,除以 \(2\)后合并回去,复杂度是 \(O(\log n)\),就可以了。

#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10;
#define ls dui[x].son[0]
#define rs dui[x].son[1]
int n,m,a[N],fa[N],dis[N];
struct node
{
	int rt,son[2],v;
}dui[N];
int find(int x)
{
	return dui[x].rt==x?x:dui[x].rt=find(dui[x].rt);
}
int merge(int x,int y)
{
	if(!x||!y)return x|y;
	if(dui[x].v<dui[y].v)swap(x,y);
	rs=merge(rs,y);
	if(dis[ls]<dis[rs])swap(ls,rs);
	dis[x]=dis[rs]+1;
	dui[ls].rt=dui[rs].rt=x;
	return x;
}
void pop(int x)
{
	dui[ls].rt=ls,dui[rs].rt=rs;
	dui[x].rt=merge(ls,rs);
}
void newd(int x)
{
	dui[x].rt=x;
	dui[x].v=a[x];
	ls=rs=0;
}
int main()
{
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	while(cin>>n)
	{
	for(int i=1;i<=n;i++)cin>>a[i],newd(i);
	cin>>m; 
	for(int i=1;i<=m;i++)
	{
		int x,y;cin>>x>>y;
		int f1=find(x),f2=find(y),f3,f4;
		if(f1==f2)
		{
			cout<<"-1"<<'\n';
			continue;
		}
		pop(f1),pop(f2);
		f3=dui[f1].rt,f4=dui[f2].rt;
		a[f1]/=2,a[f2]/=2;
		newd(f1),newd(f2);
		int ans=merge(merge(f1,f2),merge(f3,f4));
		cout<<dui[ans].v<<'\n';
	}		
	}
	return 0;
} 

P1552 [APIO2012] 派遣

比前面的题复杂一点,先考虑一个比较显然的贪心,人数一定的时候薪水越少越好,所以对题目换一种描述方式,选一个点作为管理员,在它的子树中选点使选的点数最大且薪水小于给定值,然后乘领导力。所以显然可以对每个点维护一个大根堆,由儿子向父亲转移,合并儿子们的大根堆,记录选的点数和薪水和,贪心的删掉薪水最大的点即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ls dui[x].son[0]
#define rs dui[x].son[1]
const int N=1e6+10;
ll ans,sum[N],zh[N];
int jz[N],dis[N],n,m,root;
struct node
{
	int son[2],rt,v;
}dui[N];
vector<int> ve[N]; 
int find(int x)
{
	if(dui[x].rt==0||x==0)return 0;
	return dui[x].rt==x?x:dui[x].rt=find(dui[x].rt); 
}
int merge(int x,int y)
{
	if(!x||!y)return x|y;
	if(dui[x].v<dui[y].v)swap(x,y);
	rs=merge(rs,y);
	if(dis[rs]>dis[ls])swap(rs,ls);
	dis[x]=dis[rs]+1; 
	dui[ls].rt=dui[rs].rt=x;
	return x; 
}
int pop(int x)
{
	dui[ls].rt=ls,dui[rs].rt=rs;
	dui[x].rt=merge(ls,rs);
	int jg=dui[x].v;
	dui[x].v=0; 
	return  jg;
}
void dfs(int u,int fa)
{
	int len=ve[u].size();sum[u]=1;zh[u]=dui[u].v;
	for(int i=0;i<len;i++)
	{
		int v=ve[u][i];
		dfs(v,u);
		int f1=find(u),f2=find(v);
		merge(f1,f2);
		sum[u]+=sum[v];
		zh[u]+=zh[v];
	}
	while(zh[u]>m&&sum[u])
	{
		int f1=find(u);
		sum[u]-=1;
		zh[u]-=pop(f1);
	}
	ans=max(sum[u]*jz[u],ans);
	return ;
}
int main()
{
//	freopen("P1552_4.in","r",stdin);
//	freopen("11.out","w",stdout);
	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>>dui[i].v>>jz[i]; 
		dui[i].rt=i;
		if(x==0)root=i;//找树根 
		else ve[x].push_back(i);
	}
	dfs(root,0);
	cout<<ans<<'\n';
	return 0;
}

总结

可并堆与并查集有千丝万缕的联系,它像是并查集的升级版,它的根是最大值或最小值,同时还保证了深度是\(\log\),支持\(\log\)的合并。
遇到与只联通,最值有关的,可以思考左偏树,毕竟比较简单好写不容易错。

posted @ 2024-07-19 17:18  storms11  阅读(8)  评论(0编辑  收藏  举报