堆/左偏树
堆(二叉堆)
定义
是完全二叉树,每个点有一 \(val\) ,且满足,对于给定的比较函数我们有,对于任意一个点,其与其任意儿子间的关系都满足该关系。
例:大根堆的每个点都大于它的任意儿子,小根堆则每个点都小于它的任意儿子。
STL 中有 priority_queue<>
实现了该功能。
下面以小根堆为例。别的堆可以自定义比较函数得到。
功能
一个堆支持如下操作:
- 查询堆顶元素
- 删除堆顶元素
- 插入新的元素
实现
我们需要动态维护两个性质:完全二叉树和每个儿子大于其父亲。
1.查询堆顶元素
由于我们始终保持其堆的性质,所以根节点即为堆顶元素,返回即可。
2.删除堆顶元素
我们先 swap(root,lastnode)
,并将 lastnode
位置删除,这样我们就删除了指定元素,并保持了完全二叉树的性质。但是我们遇到另外一个问题,我们在这一操作中破坏了其父子节点大小关系,这就需要我们进行调整。注意到不合法的位置有且仅有我们刚刚换上去的根,于是我们可以在它的两个儿子中选一个较小的,并将该点与其交换。但是这样堆可能仍不合法,我们只需对该子树重复该操作即可。(当然,如果堆已合法,我们不需再调整)由于其为完全二叉树,我们最多调整 \(\log n\) 次。
3.插入新的元素
我们将新加的元素直接放在完全二叉树上的第一个合法位置。然后我们又遇到了不合法的问题,我们可以参考操作二中的调整,只不过这次是自底向上调整,复杂度相同,也为 \(O(\log n)\) 。
一个支持上述三种操作的示例
struct heap { int a[mn],tp; heap() { tp=0; } void mvup(int pos) { while(pos!=1 && a[pos>>1]>a[pos]) { swap(a[pos>>1],a[pos]); pos=pos>>1; } } void mvdn(int pos) { while((pos<<1)<=tp) { if((pos<<1)==tp) { if(a[pos<<1]<a[pos])swap(a[pos],a[pos<<1]); break; } if(a[pos<<1]<a[pos<<1|1]) { if(a[pos<<1]<a[pos])swap(a[pos],a[pos<<1]); else break; pos<<=1; } else { if(a[pos<<1|1]<a[pos])swap(a[pos],a[pos<<1|1]); else break; pos=pos<<1|1; } } } void push(int x) { a[++tp]=x; mvup(tp); } void pop() { swap(a[1],a[tp]); tp--; mvdn(1); } int top() { return a[1]; } }q;
左偏树
这是一种可并堆。下面给出 OI-wiki
上的定义。
对于一棵二叉树,我们定义 外节点 为子节点数小于两个的节点,定义一个节点的 \(dist\) 为其到子树中最近的外节点所经过的边的数量。空节点的 \(dist\) 为 \(0\) 。
左偏树是一棵二叉树,它不仅具有堆的性质,并且是「左偏」的:每个节点左儿子的 \(dist\) 都大于等于右儿子的 \(dist\)。
其核心操作为 join
,即将两个点及其子树合并。下面给出以小根堆为实例的代码及注释。
node* join(node* x,node *y)//返回指针方便父亲找儿子 { if(!x || !y)return x?x:y;//如果有至少一个是空的,那就不用合并了,将非空的作为合并好的即可 node *res=NULL; if(x->val>y->val || (x->val==y->val && x->id>y->id))swap(x,y);//优先选小的点作为根,这里还要比id是因为洛谷板子要求的 res=join(x->ch[1],y);//合并右子和非根的那一部分 x->ch[1]=res;//将合并的子树接在x的右儿子上 if(res)res->fa=x; if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]);//判断dist是否满足要求,不满足则需要交换左右儿子 x->dist=(x->ch[1]?x->ch[1]->dist+1:0);//更新x的dist x->fa=NULL; return x; }
然后别的操作我们可以以 join
为基础实现:
- 加入点:将新点看作一个堆,与原堆合并
- 删除点:合并该点的左右儿子
支持合并,查堆顶,删堆顶,加点的堆的左偏树实现
struct letree { struct node { node *fa,*ch[2]; int dist,val,id; // int sz; node(int x,int nid) { dist=0,val=x; id=nid; // sz=1; fa=ch[0]=ch[1]=NULL; } }*root; // int sz; letree() { // sz=0; root=NULL; } node* join(node* x,node *y) { if(!x || !y)return x?x:y; node *res=NULL; if(x->val>y->val || (x->val==y->val && x->id>y->id))swap(x,y); res=join(x->ch[1],y); x->ch[1]=res; if(res)res->fa=x; if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]); x->dist=(x->ch[1]?x->ch[1]->dist+1:0); x->fa=NULL; return x; } void join(letree &x) { root=join(root,x.root); x.root=NULL; } bool empty() { return root==NULL; } void push(int x,int id) { node *o=new node(x,id); root=join(root,o); } int top() { return empty()?-1:root->val; } void pop() { deld[root->id]=1; root=join(root->ch[0],root->ch[1]); } }q[mn];
支持合并,查堆顶,删堆顶,加点,删除指定元素,调整某一元素大小的更为强大的左偏树实现
struct node { node *fa,*ch[2]; int dist,val; // int sz; node(int x) { dist=0,val=x; // sz=1; fa=ch[0]=ch[1]=NULL; } bool isr() { return fa->ch[1]==this; } }*now[mn];//用于存储各元素位置,方便erase int nodecnt=0; struct letree { node *root; letree() { root=NULL; } node* join(node* x,node *y)// 核心 { if(!x || !y)return x?x:y; if(x->val>y->val)swap(x,y); x->ch[1]=join(x->ch[1],y); if(x->ch[1])x->ch[1]->fa=x; if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist))swap(x->ch[0],x->ch[1]); x->dist=(x->ch[1]?x->ch[1]->dist+1:0); x->fa=NULL; return x; } void modup(node *x)//用于在erase后向上调整以保持左偏性质 { while(x) { if(x->ch[1] && (!x->ch[0] || x->ch[1]->dist>x->ch[0]->dist)) { swap(x->ch[0],x->ch[1]); x->dist=(x->ch[1]?x->ch[1]->dist+1:0); x=x->fa; } else break; } } void join(letree &x)//合并 { root=join(root,x.root); x.root=NULL; } bool empty()//还有元素吗? { return root==NULL; } int top()//堆顶是什么? { return empty()?-1:root->val; } node* pop()// 删堆顶,有返回值是方便回收 { node* res=root; root=join(root->ch[0],root->ch[1]); return res; } void push(int x)//加点 { now[++nodecnt]=new node(x); root=join(root,now[nodecnt]); } /* 也可以定义 void push(int x,int id) { now[id]=new node(x); root=join(root,now[id]); } 以指定id,但注意不能与上面那种混用,否则会出现两个元素编号相同 */ node* erase(int x)// 删除编号为x的元素,有返回值是方便回收 { if(now[x]==root)return pop();//特判没有父亲的情况 bool k=now[x]->isr(); now[x]->fa->ch[k]=join(now[x]->ch[0],now[x]->ch[1]); if(now[x]->fa->ch[k])now[x]->fa->ch[k]->fa=now[x]->fa; modup(now[x]->fa); return now[x]; } void dkey(int x,int y)//调整编号为x元素值为y { now[x]=erase(x);// 回收!! now[x]->val=y; now[x]->dist=0; now[x]->fa=now[x]->ch[0]=now[x]->ch[1]=NULL; root=join(root,now[x]); } };
本文作者:ikusiad
本文链接:https://www.cnblogs.com/ikusiad/p/18730041
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】