数据结构:线段树
@
前言
最近几天一直在线段树上面死磕,现在大概弄懂了较简单的线段树(只支持加减法的那种),来这里写篇大约的笔记,等以后学习了更多还会更新。
什么是线段树
线段树是一种二叉树,每个节点都代表一个区间的和(或者最大值等等,可以自己定义。以下用和作为例子)。比如说根节点就代表整个区间的和,下图展示了一棵完整的线段树
在这张图中,原始序列是\(1,2,3,4\),上面的树形结构就是根据这个数组构建出来的一棵线段树。
根节点的下标为\(1\),第\(i\)个节点的两个儿子的下标分别为\(i*2\),\(i*2+1\),叶节点没有儿子。每个节点的\(l\),\(r\)表示这个节点所表示的区间的左右端点,\(data\)表示这个区间中所有数的和。可以发现,一个父节点就等于两个子节点的和(当然只是对于表示和的线段树而言),根节点就代表整个数组的和。
了解了线段树是什么后,我们就来了解一下线段树的基本操作。
线段树的基本操作
\(sco\)为数据范围
大家也可以根据各自的喜好使用不同的变量名。
要注意的是线段树要开四倍大小,不然会数组越界。
怎么建造一棵线段树
线段树的结构体
如上一章所说,每个节点有三个变量分别为\(l,r,data\)。实际上还有一个变量叫做lazy,这个后文再进行介绍。所以说结构体应该这样写:
struct tree{
ll l,r,data,lz;
}tr[sco*4];
数据更新函数
前文说过,当前节点当然就是左右子节点的和嘛,作者习惯将它写成一个函数,看起来简洁,要修改时也容易,只要修改一处。
void updata(int i){
tr[i].data=tr[i*2].data+tr[i*2+1].data;
}
建树的过程可以分为三步,首先判定当前位置是不是叶子节点,是的话就赋值,否则继续建立左右子树,这个过程可以用一个递归函数实现:
void build(int i,int l,int r){
tr[i].l=l,tr[i].r=r;//确定左右区间端点
if(l==r){//判断是否为叶节点
tr[i].data=a[l];return;//是叶节点就赋值,a数组为原始序列
}
int mid((l+r)>>1);//区间的中间下标
build(i*2,l,mid);//建立左子树
build(i*2+1,mid+1,r);//建立右子树
updata(i);//更新当前节点
}
\(i\)值初始为\(1\),所以在主函数里面调用时只要写成\(build(1,1,n)\)就好了(假定原始序列左端点为\(1\),右端点为\(n\))。
单点修改与区间查询
单点修改
这个挺简单的,只要从根节点开始,判断要找的节点的下标在左子树还是右子树,直到找到该节点,然后修改完以后回溯,顺路把沿途的节点重新更新一下就好了。
void modify(int i,int dis,int k){
if(tr[i].l==tr[i].r){//如果当前为叶节点,说明找到了
tr[i].data+=k;return;//修改值然后返回
}
if(dis<=tr[i*2].r)modify(i*2,dis,k);//如果要修改的点在左子树中,就递归左节点
else modify(i*2+1,dis,k);//否则就在右子树中,递归右节点
updata(i);//修改完后更新当前节点
}
区间查询
对于每个节点,判断要查询的区间对于当前节点的左右区间是否有交集,有的话就递归,如果当前区间是要查询区间的子集,直接加上去即可,如果完全不相交,直接跳过。
int search(int i,int l,int r){
if(l<=tr[i].l && tr[i].r<=r){//如果是子集
return tr[i].data;//直接全部累加
}
if(tr[i].l>r || tr[i].r<l)return 0;//如果完全不相交,直接跳过
int s(0);//等价于 int s=0;
if(tr[i*2].r>=l)s+=search(i*2,l,r);//累加左子树的寻找结果
if(tr[i*2+1].l<=r)s+=search(i*2+1,l,r);//累加右子树的寻找结果
return s;//返回结果
}
区间修改
第四个节点变量:\(lazy\)
为什么要用\(lazy\)?
为什么不能直接修改?为什么会有个\(lazy\)?
假如我们要修改\(1 \sim 3\)区间,直接的思想就是跟区间查询一样,子集就直接加,不相交就跳过,交集就继续递归。
但这为什么是错的?
譬如原始序列是\(1,2,3,4\),我们要把\(1 \sim 3\)这个区间每个区间加上\(2\),修改完后再查询\(2 \sim 4\)的区间,过程如下图
红色为区间修改,紫色为查询。得到的答案是\(9\),可是实际上:
\(1,2,3,4\)
\(1 \sim 3\)每个\(+2\)
\(3,4,5,4\)
查询\(2 \sim 4\)的和
\(4+5+4=13\)
这与我们使用线段树求出的结果不一致,原因在于哪里?在于我们区间修改\(1 \sim 2\)时只修改了节点\(2\)的值,但是节点\(4\),\(5\)没有任何变化,所以在查询节点\(5\)时就得到了错误的答案。所以我们需要用\(lazy\)来填补这个错误。
\(lazy\)的作用
在区间修改到一个即将被修改节点时(路上经过的节点不算),打上标记,在查询到它时把这个标记向下推动,在保持时间复杂度的同时又能确保正确性。
void down(int i){
if(tr[i].lz!=0){
tr[i*2].lz+=tr[i].lz;//lazy传给左节点
tr[i*2+1].lz+=tr[i].lz;//lazy传给右节点
tr[i*2].data+=tr[i].lz*(tr[i*2].r-tr[i*2].l+1);
tr[i*2+1].data+=tr[i].lz*(tr[i*2+1].r-tr[i*2+1].l+1);
//因为是这个区间每个数同时+k,所以data要加上(这个区间的长度*父节点的lazy);
tr[i].lz=0;//父节点lazy处理完毕,归零
}
updata(i);//更新父节点
}
下图即为上文的例子加上lazy后的演示:
红色为区间修改,紫色为查询,绿色为\(lazy\)下传的过程。得到的答案是\(13\),正确。
在查询和修改时都要pushdown哦。
区间修改
void modi(int i,int l,int r,int k){
if(l<=tr[i].l && tr[i].r<=r){//如果是子集,直接用该节点累加
tr[i].data+=(tr[i].r-tr[i].l+1)*k;//加上区间长度乘乘k的值
tr[i].lz+=k;//lz加上k
return;
}
down(i);//lz下传
if(tr[i*2].r>=l)modi(i*2,l,r,k);//如果该区间与左节点有交集,递归
if(tr[i*2+1].l<=r)modi(i*2+1,l,r,k);//如果该区间与右节点有交集,递归
updata(i);
}
//将l~r每个数全部加上k
区间查询
int search(int i,int l,int r){
if(l<=tr[i].l && tr[i].r<=r){//如果是子集,直接累加
return tr[i].data;
}
if(tr[i].l>r || tr[i].r<l)return 0;//如果没有相交,返回0
down(i);//lz下传
int s(0);
if(tr[i*2].r>=l)s+=search(i*2,l,r);//如果该区间与左节点有交集,累加递归
if(tr[i*2+1].l<=r)s+=search(i*2+1,l,r);//如果该区间与右节点有交集,累加递归
return s;
}
//求l~r的和
写在最后
作者还没有学习支持乘法运算的线段树,会尽快学习,然后再来更新。有任何问题欢迎在下方评论,谢谢阅读——