算法概述
线段树是一种支持各种区间操作、区间询问的数据结构,复杂度同树状数组,时空常数略大,但应用范围极广。
树状数组基于二进制,线段树则是基于分治。
我们怎么分治?讲究几个点:
1. 1. 1. 分成左右两部分递归
2. 2. 2. 考虑合并
3. 3. 3. 考虑边界情况
那线段树的写法已经呼之欲出了。
写法
下文均以求解区间和为例。
存树
线段树真的是一棵树(不像某个树状数组长得奇形怪状),而且大概率是接近完全二叉树的,所以我一般喜欢直接用完全二叉树的方法: p ∗ 2 p*2 p∗2 是左儿子, p ∗ 2 + 1 p*2+1 p∗2+1 是右儿子,下面均以这种形式存树。
int ls(int p) {return p<<1;}
int rs(int p) {return p<<1|1;}
线段树类似于可持久化分治(什么鬼?!),就是把分治的每一步存下来,那么一个分治函数需要什么?左边界和右边界,所以这就是每个线段树节点必存的东西,另外东西看题目需要啥,我们这里需要的是这个区间的和( $lazy $ t a g tag tag 见后文)。
struct QWQ {
int l,r,d,laz;
};
合并(pushup)
分治中有一步叫合并区间,是递归完子树后要做的,其实就是更新区间和
void pushup(int p) {//合并
t[p].d=t[ls(p)].d+t[rs(p)].d;
}
建树(build)
建树就是完全的分治,一步步来:
1. 1. 1. 分成左右两部分递归
2. 2. 2. 考虑合并:边界直接赋值,其它转上文 p u s h u p pushup pushup
3. 3. 3. 考虑边界情况:只有一个点,初始权值为数组本身
void build(int p,int l,int r) {//建树
t[p].l=l,t[p].r=r;
if(l==r) {t[p].d=a[l];return;}
int m=(l+r)>>1;
build(ls(p),l,m);build(rs(p),m+1,r);
pushup(p);
}
询问(query)
仍然是简单的分治,但注意第一步并不是都要执行,一步步来:
1. 1. 1. 分成左右两部分递归:如果一个儿子区间完全在询问区间之外,那么显然没必要递归徒增复杂度
2. 2. 2. 考虑合并:返回递归的返回值之和
3. 3. 3. 考虑边界情况:现区间完全包含于询问区间,返回存储的区间和
p u s h d o w n pushdown pushdown 转下文 p u s h d o w n pushdown pushdown 。
int query(int p,int l,int r) {//区间询问
if(l<=t[p].l&&t[p].r<=r) return t[p].d;
pushdown(p);
int s=0,m=(t[p].l+t[p].r)>>1;
if(l<=m) s+=query(ls(p),l,r);
if(r>m) s+=query(rs(p),l,r);
return s;
}
修改(update)
单点
(如果是单点,请自动忽略上文与下文 p u s h d o w n pushdown pushdown 和 l a z laz laz)
单点就简单了:
1. 1. 1. 分成左右两部分递归:如果一个儿子区间不包含修改的点之外,那么显然没必要递归徒增复杂度
2. 2. 2. 考虑合并:转上文 p u s h u p pushup pushup
3. 3. 3. 考虑边界情况:现区间就是询问的点,直接修改
void update(int p,int w,int v) {//单点修改
if(t[p].l==w&&t[p].r==w) {t[p].d+=v;return;}
int m=(t[p].l+t[p].r)>>1;
if(w<=m) update(ls(p),w,v);
else update(rs(p),w,v);
pushup(p);
}
区间
现在起引入 l a z y lazy lazy t a g tag tag 。
如果区间修改我们也按和单点一样的方法,那么相当于区间中的每个点都要修改一遍,还不如暴力。
所以有一个神奇的东西叫懒惰标记。
考虑一下,如果有一个毒瘤出题人出了一组数据,全是从 1 − n 1-n 1−n 都 + 1 +1 +1 ,那么你修改了也没用到过啊,因为根本就没有询问,那么你为什么要修改呢?所以懒惰标记的思想就是:你要用了,我再修改也不迟。
(所以现在就能理解区间询问的 p u s h d o w n pushdown pushdown 了吧)
区间修改时借鉴区间询问的方法,找到一个整体区间后就停止,修改这个区间的懒惰标记。
1. 1. 1. 分成左右两部分递归:如果一个儿子区间完全在询问区间之外,那么显然没必要递归徒增复杂度
2. 2. 2. 考虑合并:转上文 p u s h u p pushup pushup
3. 3. 3. 考虑边界情况:现区间完全包含于询问区间,更新区间和与懒惰标记
关于为什么要 p u s h u p pushup pushup :因为我们的区间和是实时更新的,所以父亲的区间和也要更新。
关于为什么要 p u s h d o w n pushdown pushdown :因为我们约定 l a z y lazy lazy t a g tag tag 是给儿子看的,所以为了避免 p u s h d o w n pushdown pushdown 混淆这里也要来一次,详转下文 p u s h d o w n pushdown pushdown 。
void update(int p,int l,int r,int v) {//区间修改
if(l<=t[p].l&&t[p].r<=r) {
t[p].d+=(t[p].r-t[p].l+1)*v,t[p].laz+=v;
return;
}
pushdown(p);
int m=(t[p].l+t[p].r)>>1
if(l<=m) update(ls(p),l,r,v);
if(r>m) update(rs(p),l,r,v);
pushup(p);
}
下放(pushdown)
用处是把父亲的懒惰标记下放到儿子上,并相应地更新。
void pushdown(int p) {//下放
t[ls(p)].d+=t[p].laz*(t[ls(p)].r-t[ls(p)].l+1);
t[rs(p)].d+=t[p].laz*(t[rs(p)].r-t[rs(p)].l+1);
t[ls(p)].laz+=t[p].laz,t[rs(p)].laz+=t[p].laz;
t[p].laz=0;
}
注:代码虽然很好写,却是线段树的核心
代码
struct QWQ {int l,r,d,laz;};
struct ST {
QWQ t[4*100005];
int ls(int p) {return p<<1;}
int rs(int p) {return p<<1|1;}
void pushup(int p) {//合并
t[p].d=t[ls(p)].d+t[rs(p)].d;
}
void pushdown(int p) {//下放
t[ls(p)].d+=t[p].laz*(t[ls(p)].r-t[ls(p)].l+1);
t[rs(p)].d+=t[p].laz*(t[rs(p)].r-t[rs(p)].l+1);
t[ls(p)].laz+=t[p].laz,t[rs(p)].laz+=t[p].laz;
t[p].laz=0;
}
void build(int p,int l,int r) {//建树
t[p].l=l,t[p].r=r;
if(l==r) {t[p].d=a[l];return;}
int m=(l+r)>>1;
build(ls(p),l,m);build(rs(p),m+1,r);
pushup(p);
}
void update(int p,int l,int r,int v) {//区间修改
if(l<=t[p].l&&t[p].r<=r) {
t[p].d+=(t[p].r-t[p].l+1)*v,t[p].laz+=v;
return;
}
pushdown(p);
int m=(t[p].l+t[p].r)>>1
if(l<=m) update(ls(p),l,r,v);
if(r>m) update(rs(p),l,r,v);
pushup(p);
}
int query(int p,int l,int r) {//区间询问
if(l<=t[p].l&&t[p].r<=r) return t[p].d;
pushdown(p);
int s=0,m=(t[p].l+t[p].r)>>1;
if(l<=m) s+=query(ls(p),l,r);
if(r>m) s+=query(rs(p),l,r);
return s;
}
};
//所有l,r均为询问的区间,p为1