左偏树学习笔记
左偏树学习笔记
左偏树其实就是一个去实现可并堆这个数据结构的工具,其实完全可以用其他的写法,比如说配对堆,二叉堆,斐波那契堆等等,但是左偏树的写法更加常见,且使用起来更加方便,码量也不大。
我们一般可以用一个启发式合并实现 O(log2n) 的合并,但是这个左偏树可以实现 O(logn) 的合并。
对于一棵二叉树,我们定义 外节点 为子节点数小于两个的节点,定义一个节点的 d
为其到子树中最近的外节点所经过的边的数量。空节点的 d
为 0
对于每一个点都是关于 d
左偏的,若在操作的时候不是左偏的,那就交换两节点。
这里来介绍一些左偏树(小根堆)的功能:
既然是可并堆的写法,那最重要的就是合并了。
这里合并是把两个点先比较一下值的大小,然后将较小值(u
)的右端点设为右端点和 v
的合并后的根节点。
为什么是右端点呢,因为右边的深度较小,插到右边会使总体的深度增加较小。(实际上每次就至多增加 1
).
然后维护左偏性质,返回较小值即可。
代码也十分精简:
int merge(int u, int v) {
if (!u || !v)
return u | v;
if (val[u] > val[v])
swap(u, v);
rs[u] = merge(rs[u], v);
if (d[rs[u]] > d[ls[u]])
swap(rs[u], ls[u]);
d[u] = d[rs[u]] + 1;
return u;
}
那我们当然还需要一些辅助的数组,比如我们需要知道每一个点的根节点,这个可以使用一个并查集来写。
时间复杂度是 O(logn) 的。
那如果我们需要插入一个以前没有的节点呢,这个很好写,直接在结构体里建立一个 cnt
变量,来记录现在的节点数,每一次插入的时候直接 ++cnt
在和指定的节点 merge
即可。
如果需要实现删除操作,那就可以直接把两个儿子合并即可。
总体的模板代码:
struct LTREE { //小根堆
int ls[N],rs[N],d[N],cnt;
int val[N];
int merge(int u, int v) {
if (!u || !v)return u | v;
if (val[u] < val[v])swap(u, v);
rs[u] = merge(rs[u], v);
if (d[ls[u]] < d[rs[u]])swap(ls[u], rs[u]);
d[u] = d[ls[u]] + 1;
return u;
}
int insert(int x, int v) {
++cnt;
d[cnt] = 0, ls[cnt] = rs[cnt] = 0, val[cnt] = v;
return merge(x, cnt);
}
int calc(int x) {
return val[x];
}
int pop(int x) {
return merge(ls[x], rs[x]);
}
} lt;
但是其实还有个很大的问题,这个删除操作只能删除根节点的,一般节点删除其实也是可以的,但是作者懒得去写认为没有太大的必要,一般来说还是不太会写的。这里口胡一下,你需要每一次都像合并根节点的方式合并,然后直接跳到它的父亲节点,继续这样的操作即可,知道左右节点不需要交换即可。
这里看几道模板题。
P3377 【模板】左偏树/可并堆 - 洛谷 (luogu.com.cn)
这题直接套板子就可以了,记得有 -1
的判断。
#include<bits/stdc++.h>
using namespace std;
int flag[100005],id[100005],fa[100005];
int ls[100005], rs[100005], d[100005], cnt;
int val[100005];
int merge(int u, int v) {
if (!u || !v)
return u | v;
if (val[u] > val[v])
swap(u, v);
rs[u] = merge(rs[u], v);
if (d[rs[u]] > d[ls[u]])
swap(rs[u], ls[u]);
d[u] = d[rs[u]] + 1;
return u;
}
int insert(int x, int y) {
cnt++;
val[cnt] = y, ls[cnt] = rs[cnt] = d[cnt] = 0;
return merge(cnt, x);
}
int top(int x) {
return val[x];
}
int pop(int x) {
return merge(ls[x], rs[x]);
}
int find(int x) {
return fa[x]==x?x:fa[x]=find(fa[x]);
}
int n,m;
signed main() {
scanf("%d%d",&n,&m);
for(int i=1,x; i<=n; i++) {
scanf("%d",&x);
id[i]=insert(id[i],x);
fa[i]=i;
}
for(int i=1,op,x,y; i<=m; i++) {
scanf("%d%d",&op,&x);
if(op==1) {
scanf("%d",&y);
if(flag[x]||flag[y])continue;
x=find(x),y=find(y);
if(x!=y)fa[x]=fa[y]=merge(x,y);
} else {
if(flag[x]){
cout<<-1<<endl;
continue;
}
x=find(x);
cout<<top(x)<<endl;
flag[x]=1;
fa[x]=fa[ls[x]]=fa[rs[x]]=pop(x);
ls[x]=0,rs[x]=0,d[x]=0;
}
}
return 0;
}
P1456 Monkey King - 洛谷 (luogu.com.cn)
这题就是实现一个大根堆,每次取出堆头的元素,删除,再插入其值的一半,输出合并后的最大值即可。简单题。
[P3273 SCOI2011] 棘手的操作 - 洛谷 (luogu.com.cn)
这道题确实是一道棘手的题,比较烦的还是这个第 2
个操作和最后一个操作,第二个操作其实可以用单点修改的方法来写,只不过有点麻烦,而这个最后一个操作其实可以用一个 set
或者左偏树来维护,码量有点大。
[P4331 BalticOI 2004] Sequence 数字序列 - 洛谷 (luogu.com.cn)
这题其实主要是模型的构建,左偏树的部分还是不难的。
我们发现这个题要求{b} 是一个递增的序列,其实递增序列不太好写,而单调不减的序列会好写一点。
而有一个小技巧,把每一个数 ai 设为 ai−i, 同样的, bi 也设为 bi−i 这样我们就不用担心相邻项相等的问题了。
但是我们发现就算转化成了单调不减的形式,那还是不会做。
那不妨给自己设置一个部分分?
如果{a} 单调不减? 这个直接令 bi=ai 即可,答案为 0
。
那单调不增的情况呢?这里我们只要把 bi 设成 {a} 这个数组的中位数即可(思考为什么)。
那我们既然想到了这个的话,我们不妨把每一个单调不增的连续序列分为一块,因为如果是一个单调不减的数的话,每一个序列只有一个数,且中位数都是他本身。
但是这样就完了吗?当然不是,我们发现相邻的中位数可能不能保证单调不减,那怎么办?
这还不简单,直接把两个区间合并呀,但是其实就是把两个中位数合并即可,放在一个堆里。
对于每一个序列的单调性,可以用单调栈来维护,若现在的栈顶比栈顶下面那个数小的话就合并两个堆即可,这里的大小指的是中位数,合并之后一定要删到中位数为止,也就是实际区间大小比堆的大小大一倍才可以。
参考代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1E6+7;
int n,a[N],ans,top;
struct LTREE { //大根堆
int ls[N],rs[N],d[N],cnt;
int val[N];
int merge(int u, int v) {
if (!u || !v)return u | v;
if (val[u] < val[v])swap(u, v);
rs[u] = merge(rs[u], v);
if (d[ls[u]] < d[rs[u]])swap(ls[u], rs[u]);
d[u] = d[ls[u]] + 1;
return u;
}
int pop(int x) {
return merge(ls[x], rs[x]);
}
int insert(int x, int v) {
++cnt;
d[cnt] = 0, ls[cnt] = rs[cnt] = 0, val[cnt] = v;
return merge(x, cnt);
}
} lt;
struct node{
int l,r,rt,len,siz,val;
}st[N];
signed main() {
scanf("%lld",&n);
for(int i=1; i<=n; i++)
scanf("%lld",a+i),a[i]-=i;
for(int i=1; i<=n; i++) {
st[++top]={i,i,i,1,1,a[i]};
lt.insert(0,a[i]);
while(top >1 && st[top-1].val > st[top].val ){//合并
top--;
st[top].rt = lt.merge(st[top].rt ,st[top+1].rt);
st[top].len += st[top+1].len;
st[top].siz += st[top+1].siz;
st[top].r = st[top+1].r ;
while(st[top].siz > (st[top].len+1)/2 ){//取中位数
//cout<<st[top].val <<endl;
st[top].rt =lt.pop(st[top].rt);
st[top].siz--;
}
st[top].val=a[st[top].rt];
}
//cout<<st[1].len<<" "<<st[1].val <<endl;
}
for(int i=1;i<=top;i++)
for(int j=st[i].l ;j<=st[i].r ;j++)
ans+=abs(st[i].val -a[j]);
cout<<ans<<endl;
for(int i=1;i<=top;i++)
for(int j=st[i].l ;j<=st[i].r ;j++)
cout<<st[i].val +j<<" ";
return 0;
}