左偏树/可并堆
20231107
左偏树/可并堆
将左偏树/可并堆做一个小结,
不写我可能就要忘了。。。
左偏树,顾名思义,就是保证左子树深度一定大于右子树,同时需要满足堆的性质,
于是在合并两个堆的时候的时间复杂度就为 \(\log n\) ,
感觉是非常易懂的,具体实现的细节还是有一些。
注意我们会用到并查集和 merge 函数,
每一次合并一定是合并到右儿子,因为这样可以尽可能保证树的左右子树是相对平衡的,
从而可以大大减小时间复杂度。
具体的 merge 代码如下:
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[y]<a[x]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
return x;
}
还是比较易懂吧。主要就是想降低深度,
合并 \(n\) 大小的堆和 \(m\) 大小的堆的均摊复杂度是 \(O(\log n+\log m)\)。
P3377 【模板】左偏树/可并堆
模板题。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,fa[N],lc[N],rc[N],dep[N];
bool fl[N];
struct node{
int val,id;
bool operator <(const node &rhs) const{
if(val!=rhs.val) return val<rhs.val;
return id<rhs.id;
}
}a[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[y]<a[x]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
return x;
}
int main(){
/*2023.10.30 H_W_Y P3377 【模板】左偏树/可并堆 */
dep[0]=-1;n=read();m=read();
for(int i=1;i<=n;i++) a[i].val=read(),fa[i]=i,a[i].id=i;
for(int i=1;i<=m;i++){
int op=read(),x=read();
if(op==1){
int y=read();
if(fl[x]||fl[y]) continue;
x=find(x);y=find(y);
fa[x]=fa[y]=merge(x,y);
}
else {
if(fl[x]){puts("-1");continue;}
x=find(x);
printf("%d\n",a[x].val);
fl[x]=true;fa[lc[x]]=fa[rc[x]]=fa[x]=merge(lc[x],rc[x]);
lc[x]=rc[x]=dep[x]=0;
}
}
return 0;
}
注意删除一个数的时候的操作。
P1456 Monkey King
还是模板题,删除和合并的时候一定要记录好每一个堆的根是哪一个,否则很容易搞混。
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,lc[N],rc[N],dep[N],fa[N],a[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[x]<a[y]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
return x;
}
int main(){
/*2023.10.30 H_W_Y P1456 Monkey King 左偏树*/
while(scanf("%d",&n)!=EOF){
for(int i=1;i<=n;i++) a[i]=read(),fa[i]=i,lc[i]=rc[i]=dep[i]=0;
m=read();
for(int i=1;i<=m;i++){
int x=read(),y=read(),rt,art,brt;
x=find(x),y=find(y);
if(x==y){puts("-1");continue;}
a[x]>>=1;
rt=merge(lc[x],rc[x]);
lc[x]=rc[x]=dep[x]=0;
art=merge(rt,x);
fa[rt]=fa[x]=art;
a[y]>>=1;
rt=merge(lc[y],rc[y]);
lc[y]=rc[y]=dep[y]=0;
brt=merge(rt,y);
fa[rt]=fa[y]=brt;
rt=merge(art,brt);
fa[art]=fa[brt]=rt;
printf("%d\n",a[rt]);
}
}
return 0;
}
P2713 罗马游戏
继续模板题。
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int n,m,a[N],fa[N],lc[N],rc[N],dep[N];
bool fl[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[x]>a[y]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[rc[x]]>dep[lc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
return x;
}
int main(){
/*2023.10.30 H_W_Y P2713 罗马游戏 左偏树*/
n=read();
for(int i=1;i<=n;i++) a[i]=read(),fa[i]=i;
m=read();
for(int i=1;i<=m;i++){
char ch=getchar();
while(ch!='M'&&ch!='K') ch=getchar();
int x=read();
if(ch=='M'){
int y=read();
if(fl[x]||fl[y]) continue;
x=find(x);y=find(y);
if(x==y) continue;
fa[x]=fa[y]=merge(x,y);
}
else {
if(fl[x]){puts("0");continue;}
x=find(x);
fl[x]=true;printf("%d\n",a[x]);
fa[lc[x]]=fa[rc[x]]=fa[x]=merge(lc[x],rc[x]);
lc[x]=rc[x]=dep[x]=0;
}
}
return 0;
}
P1552 [APIO2012] 派遣
加难度了。
感觉很抽象,想到了对于每一个节点去建立一个左偏树,
但是总感觉 LCA 不一定要是这个点,但是发现题目中是只要是父亲就行了——这下这下了。
于是就直接维护最大的不断合并就可以了,如果整棵树的 \(sum\) 超了,就直接把顶部的删除就可以了。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
const int N=1e5+5;
int n,fa[N],siz[N],lc[N],rt=0,rc[N],dep[N];
ll sum[N],ans=0,m;
struct node{
int fa,c,l;
}a[N];
vector<int> g[N];
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[x].c<a[y].c) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[rc[x]]>dep[lc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
sum[x]=sum[lc[x]]+sum[rc[x]]+1ll*a[x].c;
siz[x]=siz[lc[x]]+siz[rc[x]]+1;
return x;
}
void dfs(int u){
for(auto v:g[u]){
dfs(v);
int x=find(u),y=find(v);
fa[x]=fa[y]=merge(x,y);
}
while(sum[find(u)]>m){
int x=find(u);
fa[lc[x]]=fa[rc[x]]=fa[x]=merge(lc[x],rc[x]);
dep[x]=sum[x]=siz[x]=0;
}
ans=max(ans,1ll*siz[find(u)]*a[u].l);
}
int main(){
/*2023.11.7 H_W_Y P1552 [APIO2012] 派遣 左偏树*/
n=read();m=1ll*read();
for(int i=1;i<=n;i++){
a[i].fa=read();a[i].c=read();a[i].l=read();
fa[i]=i;siz[i]=1;sum[i]=a[i].c;dep[i]=1;
if(a[i].fa==0) rt=i;
else g[a[i].fa].pb(i);
}
dfs(rt);
printf("%lld\n",ans);
return 0;
}
P3261 [JLOI2015] 城池攻占-一个堆修改
其实就是左偏树的过程中多了一个标记的 pushdown,
一定要在每一个地方都要 pd,且乘法标记的初始值是 \(-1\)。
还调了挺久,记得在删除一个元素的时候要 pd!!!
有些时候静态查错还是很有效果的。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
const int N=3e5+5;
int n,m,rt[N],lc[N],rc[N],dep[N],dis[N],ans[N];
struct tree{
ll h,tag;
int vis,fa;
}t[N];
struct node{
ll val,tag1,tag2;
int ans,st;
}a[N];
ll read(){
ll x=0,f=1ll;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void change(int x,int vis,ll tag){
if(vis==1) a[x].val+=tag,a[x].tag1+=tag;
else a[x].tag1*=tag,a[x].tag2*=tag,a[x].val*=tag;
}
void pd(int x){
if(lc[x])
change(lc[x],2,a[x].tag2),
change(lc[x],1,a[x].tag1);
if(rc[x])
change(rc[x],2,a[x].tag2),
change(rc[x],1,a[x].tag1);
a[x].tag1=0;a[x].tag2=1ll;
}
int merge(int x,int y){
if(!x||!y) return x+y;
pd(x);pd(y);
if(a[x].val>a[y].val) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
dep[x]=dep[rc[x]]+1;
return x;
}
void print(int x){
int p[15],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x) p[++tmp]=x%10,x/=10;
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar('\n');
}
int main(){
/*2023.11.7 H_W_Y P3261 [JLOI2015] 城池攻占 左偏树*/
n=(int)read(),m=(int)read();dis[0]=-1;
for(int i=1;i<=n;i++) t[i].h=read(),rt[i]=0;
for(int i=2;i<=n;i++){
t[i].fa=(int)read(),t[i].vis=(int)read()+1,t[i].tag=read();
dis[i]=dis[t[i].fa]+1;
}
for(int i=1;i<=m;i++) a[i].val=read(),a[i].st=(int)read(),dep[i]=1,a[i].tag2=1ll;
for(int i=1;i<=m;i++) rt[a[i].st]=merge(rt[a[i].st],i);
for(int u=n;u>=1;u--){
while(rt[u]&&a[rt[u]].val<t[u].h){
int x=rt[u];a[x].ans=u;
ans[u]++;pd(x);
rt[u]=merge(lc[x],rc[x]);
}
if(!rt[u]||u==1) continue;
change(rt[u],t[u].vis,t[u].tag);
rt[t[u].fa]=merge(rt[t[u].fa],rt[u]);
}
for(int i=1;i<=n;i++) print(ans[i]);
for(int i=1;i<=m;i++) print(dis[a[i].st]-dis[a[i].ans]);
return 0;
}
注意 find(0)
是会出现 MLE/RE 的奇妙错误。
P4971 断罪者-单点修改(不一定是堆顶)
这道题主要是关于不是堆顶的数如何修改。
这个时候要特别注意,建议把 fa 的改变复制算在 merge 里面去完成。
于是修改这个数的过程就是这个样子的:
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[x]<a[y]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
fa[x]=fa[lc[x]]=fa[rc[x]]=x;
dep[x]=dep[rc[x]]+1;
return x;
}
void rebuild(int x){
int l=lc[x],r=rc[x];
fa[l]=l;fa[r]=r;
lc[x]=rc[x]=dep[x]=0;
merge(find(x),merge(l,r));
}
于是主函数写起来就是比较简单的。
一定不要 find(0) ,会一直 MLE。。。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=2e6+5;
int T,W,n,m,fa[N],dep[N],lc[N],rc[N];
ll K,sum,mx;
struct node{
ll val;
int id;
bool operator <(const node &rhs) const{
if(val!=rhs.val) return val<rhs.val;
return id>rhs.id;
}
}a[N];
bool vis[N];
ll read(){
ll x=0,f=1ll;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void print(ll x){
int p[25],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x) p[++tmp]=x%10,x/=10;
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar('\n');
}
int find(int x){
if(x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
int merge(int x,int y){
if(!x||!y) return x+y;
if(a[x]<a[y]) swap(x,y);
rc[x]=merge(rc[x],y);
if(dep[lc[x]]<dep[rc[x]]) swap(lc[x],rc[x]);
fa[x]=fa[lc[x]]=fa[rc[x]]=x;
dep[x]=dep[rc[x]]+1;
return x;
}
void rebuild(int x){
int l=lc[x],r=rc[x];
fa[l]=l;fa[r]=r;
lc[x]=rc[x]=dep[x]=0;
merge(find(x),merge(l,r));
}
void sol(){
n=(int)read();m=(int)read();
for(int i=1;i<=n;i++) a[i].val=read(),a[i].id=i,fa[i]=i,lc[i]=rc[i]=dep[i]=0;
fa[0]=dep[0]=lc[0]=rc[0]=0;
memset(vis,false,sizeof(vis));
for(int i=1;i<=m;i++){
int op=(int)read(),A=(int)read();
if(op==2){
a[A].val=0;
rebuild(A);
}
if(op==3){
ll B=read();int x=find(A);
a[x].val=a[x].val>B?a[x].val-B:0;
rebuild(x);
}
if(op==4){
int B=(int)read();
int x=find(A),y=find(B);
if(x==y) continue;
merge(x,y);
}
}
sum=0;mx=0;
for(int i=1;i<=n;i++)
if(!vis[find(i)]) sum+=a[find(i)].val,mx=max(mx,a[find(i)].val),vis[find(i)]=true;
if(W==2) sum-=mx;
if(W==3) sum+=mx;
if(sum==0) printf("Gensokyo ");
else if(sum>K) printf("Hell ");
else printf("Heaven ");
print(sum);
}
int main(){
/*2023.11.7 H_W_Y P4971 断罪者 左偏树*/
T=(int)read();W=(int)read();K=read();
while(T--) sol();
return 0;
}
P3273 [SCOI2011] 棘手的操作-找不到家了
被列到题单里面,但是不太能左偏树——左偏树似乎会被卡。
于是我考虑了另外一种做法。
离线,按照某种规定重新排序之后去用线段树维护即可。
至于这个某种规定就是你把一个块里面的排在一起就行啦~
想还是挺好想的吧。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=3e5+5;
const ll inf=1e18;
int n,m,rev[N],id[N],fa[N],L[N],R[N],idx=0,nxt[N];
ll a[N];
struct query{
int op,x,y;
ll val;
}q[N];
ll read(){
ll x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-') f=-1;ch=getchar();}
while(isdigit(ch)){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return x*f;
}
void print(ll x){
int p[25],tmp=0;
if(x==0) putchar('0');
if(x<0) putchar('-'),x=-x;
while(x) p[++tmp]=x%10,x/=10;
for(int i=tmp;i>=1;i--) putchar(p[i]+'0');
putchar('\n');
}
int find(int x){
if(fa[x]==x) return x;
return fa[x]=find(fa[x]);
}
void merge(int x,int y){
x=find(x);y=find(y);
if(x==y) return;
nxt[R[x]]=L[y];
R[x]=R[y];fa[y]=x;
return;
}
struct sgt{
ll tag,mx;
}tr[N<<2];
#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc
void pu(int p){tr[p].mx=max(tr[lc].mx,tr[rc].mx);}
void pd(int p){
if(!tr[p].tag) return;
tr[lc].tag+=tr[p].tag;
tr[rc].tag+=tr[p].tag;
tr[lc].mx+=tr[p].tag;
tr[rc].mx+=tr[p].tag;
tr[p].tag=0;
}
void build(int l,int r,int p){
if(l==r) return tr[p].mx=a[id[l]],void();
build(lson);build(rson);pu(p);
}
void upd(int l,int r,int p,int x,int y,ll val){
if(x<=l&&y>=r){
tr[p].tag+=val;
tr[p].mx+=val;
return ;
}
pd(p);
if(x<=mid) upd(lson,x,y,val);
if(y>mid) upd(rson,x,y,val);
pu(p);
}
ll qry(int l,int r,int p,int x,int y){
if(x<=l&&y>=r) return tr[p].mx;
pd(p);ll res=-inf;
if(x<=mid) res=max(res,qry(lson,x,y));
if(y>mid) res=max(res,qry(rson,x,y));
return res;
}
void sol(){
n=(int)read();
for(int i=1;i<=n;i++) a[i]=read(),L[i]=R[i]=fa[i]=i,nxt[i]=0;
m=(int)read();
for(int i=1;i<=m;i++){
char ch=getchar(),s;
while(ch!='A'&&ch!='U'&&ch!='F') ch=getchar();
if(ch=='U'){
int x=(int)read(),y=(int)read();
merge(x,y);
q[i].x=x;q[i].y=y;q[i].op=1;
}
if(ch=='A'){
s=getchar();
if(s=='1') q[i].op=2,q[i].x=(int)read(),q[i].val=read();
if(s=='2') q[i].op=3,q[i].x=(int)read(),q[i].val=read();
if(s=='3') q[i].op=4,q[i].val=read();
}
if(ch=='F'){
s=getchar();
if(s=='1') q[i].op=5,q[i].x=(int)read();
if(s=='2') q[i].op=6,q[i].x=(int)read();
if(s=='3') q[i].op=7;
}
}
idx=0;
for(int i=1;i<=n;i++)
if(find(i)==i){
int nw=L[i];
while(nw){
id[++idx]=nw;
rev[nw]=idx;
if(nw==R[i]) break;
nw=nxt[nw];
}
}
build(1,n,1);
for(int i=1;i<=n;i++) L[i]=R[i]=fa[i]=i,nxt[i]=0;
for(int i=1;i<=m;i++){
if(q[i].op==1) merge(q[i].x,q[i].y);
if(q[i].op==2) upd(1,n,1,rev[q[i].x],rev[q[i].x],q[i].val);
if(q[i].op==3) upd(1,n,1,rev[L[find(q[i].x)]],rev[R[find(q[i].x)]],q[i].val);
if(q[i].op==4) upd(1,n,1,1,n,q[i].val);
if(q[i].op==5) print(qry(1,n,1,rev[q[i].x],rev[q[i].x]));
if(q[i].op==6) print(qry(1,n,1,rev[L[find(q[i].x)]],rev[R[find(q[i].x)]]));
if(q[i].op==7) print(tr[1].mx);
}
}
int main(){
/*2023.11.7 H_W_Y P3273 [SCOI2011] 棘手的操作 乱搞*/
sol();return 0;
}
左偏树到此为止!