左偏树
左偏树是可并堆的一种,支持在O(log2(N))的时间复杂度内进行合并的堆式数据结构。
定义外结点:lc或rc为空结点。
定义零距离:x的距离dis[x]为x子树内与x最近的外结点到x的距离,空结点dis=-1.
左偏树具有堆性质,若为小根堆,对于所有x,v[x]<=v[lc],v[x]<=v[rc]。
左偏树具有左偏性质,对于所有x,dis[lc]>=dis[rc],有结论,dis[x]=dis[rc]+1。
在不需要维护严格的父子关系,只需要知道所在集合的根节点时,可以路径压缩并查集。
struct LeftistTree{
struct tree{
int l,r,v,d,f;
}t[N];
#define l(p) (t[p].l)
#define r(p) (t[p].r)
#define v(p) (t[p].v)
#define d(p) (t[p].d)
#define f(p) (t[p].f)
int tot;
inline LeftistTree(){
tot=0;
d(0)=-1;
}
int find(int x){/*路径压缩并查集*/
return x==f(x)?x:f(x)=find(f(x));
}
inline int top(int x){/*返回p所在小根堆的最小值*/
if(v(x)==-1)return -1;
return v(find(x));
}
inline int pop(int p){/*返回最小值并删除*/
if(v(p)==-1)return -1;
p=find(p);/*最小值也就是根节点*/
int x=v(p);
f(l(p))=f(r(p))=f(p)=merge(l(p),r(p));/*删除节点就是合并左右儿子,三者的父亲为了路径压缩,都要指向新的根节点*/
l(p)=r(p)=d(p)=0;/*清空所有儿子和dis*/
v(p)=-1;/*权值标记为删除*/
return x;
}
int merge(int x,int y){/*合并x和y两棵子树*/
if(!x||!y)return x|y;/*其一为空则返回*/
if(v(x)>v(y)||(v(x)==v(y)&&x>y))swap(x,y);/*构建小根堆,强制x为合并后的根节点*/
r(x)=merge(r(x),y);/*y与x的一个儿子进行合并并代替原来x的儿子*/
if(d(l(x))<d(r(x)))swap(l(x),r(x));/*如果不满足左偏性质则交换两个儿子*/
d(x)=d(r(x))+1;/*维护距离*/
return x;
}
inline void mergeset(int x,int y){/*合并x和y所在的可并堆*/
if(v(x)==-1||v(y)==-1)return;/*两个数已经被删除则不进行操作*/
x=find(x),y=find(y);/*先找到根节点*/
if(x==y)return;
f(x)=f(y)=merge(x,y);/*并查集合并根节点*/
}
inline void create(int x){/*创建一个初始只含x的小根堆*/
t[++tot]={0,0,x,0,tot};
}
inline void push(int p,int x){/*在p所在堆中加入x*/
t[++tot]={0,0,x,0,tot};/*先为x新建一个堆*/
p=find(p);
f(p)=f(tot)=merge(p,tot);/*合并*/
}
inline int modify(int p,int v){/*修改p所在堆的根节点的权值*/
p=find(p);/*找到根节点*/
v(p)+=v;/*修改权值*/
int x=merge(l(p),r(p));/*合并子树为新根x*/
l(p)=r(p)=d(p)=0;/*孤立旧根*/
f(p)=f(x)=merge(p,x);/*合并新旧根节点为总根*/
return f(p);/*返回合并后的总根*/
}
}lt;
可持久化左偏树,在进行merge时新建节点并对该节点进行操作即可。
int merge(int x,int y){
if(!x||!y)return x|y;
if(v(x)>v(y))swap(x,y);
int p=++tot;
t[p]=t[x];
r(p)=merge(r(p),y);
if(d(l(p))<d(r(p)))swap(l(p),r(p));
d(p)=d(r(p))+1;
return p;
}
左偏树也可以随机合并,随机决定是否交换儿子。
int merge(int x,int y){
if(!x||!y)return x|y;
if(v(x)>v(y))swap(x,y);
r(x)=merge(r(x),y);
if(rand()&1)swap(l(x),r(x));
d(x)=d(r(x))+1;
return x;
}
如果维护的父指针是指向父亲节点的,那么每次merge时需要增加pushup操作。
void pushup(int p){
if(!p||d(p)==d(r(p))+1)return;
d(p)=d(r(p))+1;
pushup(f(p));
}
int merge(int x,int y){
if(!x||!y)return x|y;
if(v(x)>v(y))swap(x,y);
r(x)=merge(r(x),y);
if(d(l(x))<d(r(x)))swap(l(x),r(x));
f(l(x))=f(r(x))=x;
pushup(x);
return x;
}
维护多个大根堆,支持一种操作,将两个大根堆的根节点减少为原来的一半,之后合并两个大根堆并返回合并后的最大值。对于修改根节点的操作,可以先修改权值,之后取出根节点,再重新把根节点加入原来的堆中。
inline int modify(int p){
p=find(p);/*找到根节点*/
v(p)>>=1;/*修改权值*/
int x=merge(l(p),r(p));/*合并子树为新根x*/
l(p)=r(p)=d(p)=0;/*孤立旧根节点*/
return f(p)=f(x)=merge(p,x);/*合并旧根与新根为总根*/
}
inline int solve(int x,int y){
x=find(x),y=find(y);
if(x==y)return -1;
x=modify(x),y=modify(y);
f(x)=f(y)=merge(x,y);/*合并x与y*/
return v(f(x));
}
一棵有根树,每个点有领导力与费用,选一个点当领导,之后在这个点的子树中选择费用之和不超过m的点,利润为领导的领导力*选择的点的个数,领导可不被选择,求利润最大值。维护多个大根堆,从叶子节点往父亲那里合并,所有儿子合并完后,不断删除费用最大的人,直到费用总和<=m,即为该点的最优方案。
inline void pushup(int p){
d(p)=d(r(p))+1;
sm(p)=sm(l(p))+sm(r(p))+v(p);
sz(p)=sz(l(p))+sz(r(p))+1;
}
inline int split(int p){
return merge(l(p),r(p));
}
int merge(int x,int y){
if(!x||!y)return x|y;
if(v(x)<v(y))swap(x,y);
r(x)=merge(r(x),y);
if(d(l(x))<d(r(x)))swap(l(x),r(x));
pushup(x);
return x;
}
void dfs(int x){
v(x)=sm(x)=cost[x];
l(x)=r(x)=d(x)=0;
sz(x)=1;
rt[x]=x;
for(auto y:v[x]){
dfs(y);
rt[x]=merge(rt[x],rt[y]);
}
while(sm(rt[x])>m)rt[x]=split(rt[x]);
ans=max(ans,sz(rt[x])*lead[x]);
}
n个城池组成一课有根树,i收fi管辖。有m个骑士,初始战斗力为si,第一个攻击的城池为ci,每个城池有一个防御力hi,若骑士战斗力>=hi则可以占领,否则会牺牲,占领后骑士战斗力会发生变化,继续攻击fi,知道1号城池或牺牲,对于没错城池,若ai=0,攻占后战斗力增加vi,若ai=1,攻占后战斗力乘以vi,每个骑士单独计算,问每个城市有多少个骑士在这里牺牲,每个骑士攻占多少个城池。讲同出生城池的骑士合并为一堆,从下到上遍历城池树,一直用当前骑士团最弱的和hi进行比较,死的弹出,没死的放到父节点并更改战斗力。左偏树维护最小值和标记下放,注意要先乘法下放再加法下放。
inline void add(int p,int x){
v(p)+=x;
a(p)+=x;
}
inline void mul(int p,int x){
v(p)*=x;
a(p)*=x;
m(p)*=x;
}
inline void pushdown(int p){
mul(l(p),m(p));
add(l(p),a(p));
mul(r(p),m(p));
add(r(p),a(p));
a(p)=0;
m(p)=1;
}
int merge(int x,int y){
if(!x||!y)return x|y;
pushdown(x),pushdown(y);
if(v(x)>v(y))swap(x,y);
r(x)=merge(r(x),y);
if(d(l(x))<d(r(x)))swap(l(x),r(x));
d(x)=d(r(x))+1;
return x;
}
inline int split(int p){
pushdown(p);
return merge(l(p),r(p));
}
void dfs(int x){
for(int i=g.h[x];i;i=g.e[i].next){
int y=g.e[i].to;
dep[y]=dep[x]+1;
dfs(y);
}
while(rt[x]&&v(rt[x])<h[x]){
a1[x]++;
a2[rt[x]]=dep[c[rt[x]]]-dep[x];
rt[x]=split(rt[x]);
}
if(a[x])mul(rt[x],v[x]);
else add(rt[x],v[x]);
if(x>1)rt[fa[x]]=merge(rt[x],rt[fa[x]]);
else while(rt[x]){
a2[rt[x]]=dep[c[rt[x]]]+1;
rt[x]=split(rt[x]);
}
}
for(int i=1;i<=m;i++){
int x;
cin>>x>>c[i];
lt.create(x);
lt.rt[c[i]]=lt.merge(i,lt.rt[c[i]]);
}
给定一个序列a,求一个递增序列b,使得sum(i=1->n)(|a[i]-b[i]|)最小。若a是一个单调不降的序列,那么可以取b[i]=a[i],若a单调不升,那么取b[i]的a的中位数为最优,于是将序列分成一些单调不升的几段,每段都取中位数,但是这些中位数组成的序列可能并不是单调不降的,所以需要合并两个区间并重新取中位数。对于求区间中位数,就是所有元素入堆,病弹出最大值知道只剩一半元素。合并区间的前提是该区间中位数比后面区间中位数大。对于每个ai,减去i并不会影响,于是可以转化成单调不降的b数列。
struct node{
int l,r,sz,rt,v;
}s[N];
for(int i=1;i<=n;i++){
int x;
cin>>x;
lt.create(x-i);
}
for(int i=1;i<=n;i++){
s[++top]={i,i,1,i,lt.t[i].v};
while(top>1&&s[top-1].v>s[top].v){
top--;
s[top].rt=lt.merge(s[top].rt,s[top+1].rt);
s[top].sz+=s[top+1].sz;
s[top].r=s[top+1].r;
while(s[top].sz>(s[top].r-s[top].l+2)/2){
s[top].sz--;
s[top].rt=lt.split(s[top].rt);
}
s[top].v=lt.t[s[top].rt].v;
}
}
for(int i=1,p=1;i<=n;i++){
p+=(i>s[p].r);
ans+=abs(s[p].v-lt.t[i].v);
}
cout<<ans<<'\n';
for(int i=1,p=1;i<=n;i++){
p+=(i>s[p].r);
cout<<s[p].v+i<<' ';
}
支持三中操作,将某个点的权值减小到0,将某个集合最大值减小到某个值,合并两个集合。左偏树维护大根堆,父指针记录根节点,并不需要维护父亲,对于修改一个点的点权,先独立该节点,合并该节点的子树,之后将该节点重新加入堆中。
inline void extract(int p){/*将p节点提取出堆并重新加入,调整p在堆中的位置*/
int x=merge(l(p),r(p));
l(p)=r(p)=d(p)=0;
f(l(p))=f(r(p))=f(p)=f(x)=f(find(p))=merge(x,find(p));
}
while(m--){
int op,a,b;
cin>>op>>a;
if(op!=2)cin>>b;
if(op==2){
lt.t[a].v=0;
lt.extract(a);
}
else if(op==3){
a=lt.find(a);
lt.t[a].v-=lt.t[a].v>b?b:lt.t[a].v;
lt.extract(a);
}
else if(op==4)lt.mergeset(a,b);
}