左偏树
左偏树是可并堆的一种(除了左偏树别的我不会)。可并堆具有堆的性质,可以快速合并。
首先我们回想一些我们比较熟悉的东西。
1.优先队列
我们通常用优先队列堆优化(小根堆)时是这样:
priority_queue<int,vector<pair<int,int> > ,greater<pair<int,int> > >q;
我们会得到按照加入的\(dis\)\((first)\)从小到大排好序的队列。其中,\(dis\)就是键值。
2.二叉堆
从二叉堆的结构说起,它是一棵二叉树,并且是完全二叉树,每个结点中存有一个元素(或者说,有个权值)。
堆性质:父亲的权值不小于儿子的权值(大根堆)。同样的,还有小根堆。 ——by oi wiki
其实我感觉可并堆和二叉堆就是多了个可以快速合并(
左偏树的定义
左偏树是一种可并堆的实现。左偏树是一棵二叉树,它的节点除了和二叉树的节点一样具有左右子树指针\(( left, right )\)外,还有两个属性:键值\((key)\)和距离\((dist)\)。键值用于比较节点的大小。
dist的定义和性质:
当节点i的左子树或右子树为空时,称节点i称为外节点\((external node)\)。
节点\(i\)的距离\((dist(i))\)是节点i到它的后代中,最近的外节点所经过的边数。
特别的,如果节点\(i\)本身是外节点,则它的距离为\(0\)。
而空节点的距离规定为\(-1\) 。有时提到一棵左偏树的距离,这指的是该树根节点的距离。
看一个左偏树的图\(\Downarrow\)
外节点\(\Downarrow\)
基本性质
val表示key,即键值
左偏:
每个节点左儿子的dist都大于等于右儿子的dist。
因此,左偏树每个节点的dist都等于其右儿子的dist加一。
但是,dist不是深度,一条向左的链也是左偏树。
性质1:
满足堆的性质,对于任意一点p,\(val[p]\ge(\le)val[l],val[r]\)。
性质2:
\(dist[x]=dist[r]+1\),也就是说,任意节点的距离等于其右子树的距离+1。
由左偏性质以及外节点的距离为0可以得出。
定理及引理
定理:
对于一个有\(n\)个节点的左偏树,最大距离不超过\(\log(n+1)−1\),即\(max\{dist\}\leq \log(n+1)-1\)。
证明: 设\(max\{dist\}=k\),那么显然节点数最少的左偏树是一棵完全二叉树,此时节点数为\(2^{k+1}-1\),故\(n\geq 2^{k+1}-1\),移项得:\(k\leq \log(n+1)-1\)。
引理:
如果一棵左偏树的距离为k,则这颗左偏树节点数最少为\(2^{k+1}−1\)。
证明: 因为左偏树的左子树距离一定大于等于右子树的距离,并且左偏树的距离等于右子树距离加一,那么一颗左偏树的距离一定且要求节点数最少时,节点数最少时任意节点的左子树大小等于右子树大小,满足这样性质的二叉树就是完全二叉树。
这个大家试着理解一下,暂时不理解也没关系。后面的内容没怎么提到。
几个简单的基本操作
合并(merge)
合并两个堆时,由于要满足堆性质,先取值较小(或大)的那个根作为合并后堆的根节点,然后将这个根的左儿子作为合并后堆的左儿子,递归地合并其右儿子与另一个堆,作为合并后的堆的右儿子。
为了满足左偏性质,合并后若左儿子的\(dist\)小于右儿子的\(dist\),就交换两个儿子。
代码实现
int merge(int x,int y){
if(!x||!y) return x+y;//若有一堆为空 则返回另一个
if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);//满足堆性质 取较小值 若值相同 使x编号较小 直接写成if(t[x].val>t[y].val)也行
//x的做儿子作为合并后堆的做儿子,递归合并右儿子和右子树
t[x].r=merge(t[x].r,y);
t[t[x].r].fa=x;//更新父亲
if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);//左偏 左儿子大于右儿子
t[x].dist=t[t[x].r].dist+1;//跟新根值
return x;
}
主函数中
int X=Find(x),Y=Find(y);
if(X!=Y)
t[X].fa=t[Y].fa=merge(X,Y);
插入节点
把单点视为一个堆,按上面的方法合并即可。
代码实现
t[x].fa=t[y].fa=merge(x,y);//x是插入的点,y是要插入的堆
删除根(pop)
把根的\(val=-1\)(标记为被删除),合并根的左右儿子即可。
代码实现
void pop(int x){
t[x].val=-1;//标为被删除
t[t[x].l].fa=t[x].l;//更新两孩子父亲为孩子本身,相当于把x删除
t[t[x].r].fa=t[x].r;
t[x].fa=merge(t[x].l,t[x].r);//更新x的孩子的父节点,把重新合并后的根赋值给x的父节点
}
【模板】左偏树(可并堆
分析
题目说了是个模板
用到的只有基本的合并和删除操作。
是个小根堆。
没啥好说的直接看代码吧((
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,op,x,y;
struct node{
int l,r,dist,val,fa;
}t[N];
int Find(int x){//并查集查询操作
if(t[x].fa==x)return x;
t[x].fa=Find(t[x].fa);
return t[x].fa;
}
int merge(int x,int y){
if(!x||!y) return x+y;//若有一堆为空 则返回另一个
if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);//满足堆性质 取较小值 若值相同 使x编号较小
//x的做儿子作为合并后堆的做儿子,递归合并右儿子和右子树
t[x].r=merge(t[x].r,y);
t[t[x].r].fa=x;//更新父亲
if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);//左偏 左儿子大于右儿子
t[x].dist=t[t[x].r].dist+1;//跟新根值
return x;
}
void pop(int x){
t[x].val=-1;//标为被删除
t[t[x].l].fa=t[x].l;//更新两孩子父亲为孩子本身,相当于把x删除
t[t[x].r].fa=t[x].r;
t[x].fa=merge(t[x].l,t[x].r);//更新x的孩子的父节点,把重新合并后的根赋值给x的父节点
}
int main()
{
cin>>n>>m;
t[0].dist=-1;//为什么初始化为-1?为了保证叶子节点的dist为0
for(int i=1;i<=n;i++)cin>>t[i].val,t[i].fa=i;//输入顺便初始化
for(int i=1;i<=m;i++){
cin>>op>>x;
if(op==1){
cin>>y;
if(t[x].val==-1||t[y].val==-1)continue;
int X=Find(x),Y=Find(y);
if(X!=Y)
t[X].fa=t[Y].fa=merge(X,Y);
}
else {
if(t[x].val==-1) {puts("-1");}//如果已经被删除了直接输出-1
else {
int X=Find(x);
printf("%d\n",t[X].val);//输出堆顶元素
pop(X);//删除堆顶元素
}
}
}
return 0;
}
罗马游戏
分析
题目说的花里胡哨的 其实还是个板子
简化题意:命令为\(M\)时把\(i\),\(j\)合并。
命令为\(K\)时删除分数最低元素,故为一个小根堆。
注意,若已经被删除了,要输出\(0\),而不是\(-1\)。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int n,m,x,y;
struct node{
int l,r,fa,val,dist;
}t[N];
char op;
int merge(int x,int y){
if(!x||!y) return x+y;
if(t[x].val>t[y].val||t[x].val==t[y].val&&x>y)swap(x,y);
t[x].r=merge(t[x].r,y);
t[t[x].r].fa=x;
if(t[t[x].l].dist<t[t[x].r].dist)swap(t[x].l,t[x].r);
t[x].dist=t[t[x].r].dist+1;
return x;
}
int Find(int x){
if(t[x].fa==x) return x;
t[x].fa=Find(t[x].fa);
return t[x].fa;
}
void pop(int x){
t[x].val=-1;
t[t[x].l].fa=t[x].l;
t[t[x].r].fa=t[x].r;
t[x].fa=merge(t[x].l,t[x].r);
}
int main()
{
scanf("%d",&n);
t[0].dist=-1;
for(int i=1;i<=n;i++)
scanf("%d",&t[i].val),t[i].fa=i;
scanf("%d",&m);
for(int i=1;i<=m;i++){
cin>>op;scanf("%d",&x);
if(op=='M'){
scanf("%d",&y);
if(t[x].val==-1||t[y].val==-1) continue;
int X=Find(x),Y=Find(y);
if(X!=Y)
t[X].fa=t[Y].fa=merge(X,Y);
}
else {
if(t[x].val==-1){puts("0");}
else {
int X=Find(x);
printf("%d\n",t[X].val);
pop(X);
}
}
}
return 0;
}
其实可以发现,和第一个例题不一样的地方只有输入那里。这其实是我直接复制的模板然后改了改。
Monkey King
分析
是不是很熟悉? 是的没错,这就是夏令营\(day1\)的例题。
简化题意:使\(x\)和\(y\)的\(val\)减半后重新合并,并输出合并后堆的根。
若两个猴子在同一个堆中,输出\(-1\)。
另一个与前两个题不同的地方是,这个题是个大根堆。
因为每次都是强壮值最大的进行战斗。
所以在合并时,只需要把
if(t[x].val>t[y].val)swap(x,y);
改为
if(t[x].val<t[y].val) swap(x,y);
即可。
注意,这个题是多测,记得清空数组。
是不是很简单!
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,m,x,y;
struct node{
int l,r,fa,val,dist;
}t[N];
int Find(int x){
if(t[x].fa==x) return x;
t[x].fa=Find(t[x].fa);
return t[x].fa;
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(t[x].val<t[y].val) swap(x,y);
t[x].r=merge(t[x].r,y);
t[t[x].r].fa=x;
if(t[t[x].l].dist<t[t[x].r].dist) swap(t[x].l,t[x].r);
t[x].dist=t[t[x].r].dist+1;
return x;
}
int pop(int x){
t[x].val>>=1;
int new_root=merge(t[x].l,t[x].r);
t[x].l=t[x].r=0;t[x].dist=-1;
return t[x].fa=t[new_root].fa=merge(new_root,x);
}
int main()
{
while(cin>>n){
memset(t,0,sizeof(t));
t[0].dist=-1;
for(int i=1;i<=n;i++){
scanf("%d",&t[i].val);
t[i].fa=i;
}
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%d%d",&x,&y);
int X=Find(x),Y=Find(y);
if(X==Y){
puts("-1");
}
else {
int l=pop(X),r=pop(Y);
t[l].fa=t[r].fa=merge(l,r);
printf("%d\n",t[t[l].fa].val);
}
}
}
return 0;
}
左偏树当然不止这些 剩下的操作大家完全可以自学了((