左偏树/可并堆
简介
什么是左偏树?
上面的树都是左偏树。
先引出一个概念,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个点。
因为上面这个性质可得左偏树的深度有限制,两个左偏树进行合并可以用\(Θ(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\)的合并。
遇到与只联通,最值有关的,可以思考左偏树,毕竟比较简单好写不容易错。