前言(废话,水长度)
以下内容可以直接跳过到定义!
如果你看过我前几期博客,细心的朋友可以发现我并不喜欢加注释,但数据结构这个专题下的博客我要学习lhy26老师严谨认真但不高产的写博客风格,要细细的分开慢慢解说,当然,汇总版我是不会加注释的,毕竟不加注释就看不出抄题解我是不会告诉你们lhy26老师光平衡树一篇博客就写了半年多还没写完
定义
线段树(segment tree),顾名思义,是每个节点都是线段即区间的二叉树,用来维护一些区间操作的数据结构
具体实现
接下来的所有操作都以例题P3372 【模板】线段树 1的操作为准
线段树结构体数组的定义
线段树其实也可以用一堆数组堆起来,但是我个人喜欢结构体的写法,简洁又美观不过我懒得封装了qwq,但无论用哪种存储方式,记得开四倍的空间
点击查看代码
struct Segment_tree{
int l,r;//表示区间的左端点和右端点
ll tg;//lazy tag后文介绍
ll val;//区间维护的值,此题为区间和
Segment_tree()//构造函数
{
return;
}
};
Segment_tree tree[MAXN<<2];
构建一棵线段树
考虑构建一个[l,r]的区间的线段树,其中每个数的值分别为a[l]~a[r],这时我们取一个中间mid=l+r>>1
对区间[l,mid]和[mid+1,r]分别进行递归操作,将左子树和右子树的值汇总到该节点中,递归到叶子节点即没有儿子时返回
点击查看代码
void build(int l=1,int r=n,int p=1)//l是当前区间的左端点,默认为1,r是区间的右端点
//默认是n,p是当前区间在结构体数组的下标,默认为1
{
tree[p].l=l;//左端点赋值
tree[p].r=r;//右端点赋值
if(l==r)//是叶子节点
{
tree[p].val=a[l];//特判赋值
return;
}
int mid=l+r>>1;//取中间值
build(l,mid,p<<1);//递归调用左孩子
build(mid+1,r,p<<1|1);//递归调用右孩子
tree[p].val=tree[p<<1].val+tree[p<<1|1].val;//将和上传
return;
}
lazy tag和下传标记
在看我build的代码时会发现我的注释中提到了一个叫lazy tag的东西,其实线段树的精髓和难点就在lazy tag,使用lazy tag能节省大量暴力算法浪费的时间,使得线段树的速度远超暴力速度,lazy tag的原理非常简单,就是假设我要对当前下标为p的区间进行区间加上一个值,这是我们就可以给p这个区间打上lazy tag,这样只需在需要调用p的孩子时下传标记即可
点击查看代码
void spread(int p)
{
if(tree[p].tg)//如果p没有标记就不下传,节省时间
{
tree[p<<1].val+=(tree[p<<1].r-tree[p<<1].l+1)*tree[p].tg;//r-l+1是元素个数,是对左孩子值的处理
tree[p<<1|1].val+=(tree[p<<1|1].r-tree[p<<1|1].l+1)*tree[p].tg;//对右孩子值的处理
tree[p<<1].tg+=tree[p].tg;//将标记传给左儿子
tree[p<<1|1].tg+=tree[p].tg;//将标记传给右孩子
tree[p].tg=0;//标记传完,赋成0
}
return;
//在这个时候将就意味着随后的所有需要调用孩子的地方都得下传标记
}
区间修改,此题为区间加
我们只需先下传标记,再递归调用左右孩子,只要左子树被有一部分就调用左儿子,右儿子同理,再将左右孩子的和上传,改变现在的值,递归到现在的区间完全被修改的区间覆盖即该区间内所有元素都要执行区间加时直接修改并打上lazy tag,并返回
点击查看代码
void update(int l,int r,ll val,int p=1)//要修改的区间的左端点l,右端点r,值val
{
if(l<=tree[p].l && tree[p].r<=r)//被覆盖
{
tree[p].val+=(tree[p].r-tree[p].l+1)*val;//直接修改
tree[p].tg+=val;//打上lazy tag
return;
}
spread(p);//下传标记
int mid=tree[p].l+tree[p].r>>1;//取中间值
if(l<=mid)//左儿子有一部分
update(l,r,val,p<<1);//递归调用左孩子
if(mid<r)//右儿子有一部分
update(l,r,val,p<<1|1);//递归调用右孩子
tree[p].val=tree[p<<1].val+tree[p<<1|1].val;//上传儿子的和
return;
}
区间汇总,此题为区间求和
先下传标记,再分别递归调用左右孩子,如果左孩子的一部分被覆盖则调用左孩子,右孩子同理,将两个孩子的和汇总到变量ans
中,返回ans
的值,递归到当前区间全部被覆盖即当前的区间和可以直接累加在最终的结果中时直接返回tree[p].val
点击查看代码
ll query(int l,int r,int p=1)//查询的左端点l,右端点r
{
if(l<=tree[p].l && tree[p].r<=r)//被覆盖
{
return tree[p].val;//直接返回
}
spread(p);//下传标记
int mid=tree[p].l+tree[p].r>>1;//取中间值
ll ans=0;//将和赋成初始值0
if(l<=mid)//左孩子部分覆盖
ans+=query(l,r,p<<1);//累加答案
if(mid<r)//右孩子部分覆盖
ans+=query(l,r,p<<1|1);//累加答案
return ans;//返回值
}
每日一板
板子题:P3372 【模板】线段树 1
上代码!
点击查看代码
#include<iostream>
using namespace std;
const int MAXN=1e5+10;
typedef long long ll;
int n,m;
ll a[MAXN];
struct Segment_tree{
int l,r;
ll tg;
ll val;
Segment_tree()
{
return;
}
};
Segment_tree tree[MAXN<<2];
void build(int l=1,int r=n,int p=1)
{
tree[p].l=l;
tree[p].r=r;
if(l==r)
{
tree[p].val=a[l];
return;
}
int mid=l+r>>1;
build(l,mid,p<<1);
build(mid+1,r,p<<1|1);
tree[p].val=tree[p<<1].val+tree[p<<1|1].val;
return;
}
void spread(int p)
{
if(tree[p].tg)
{
tree[p<<1].val+=(tree[p<<1].r-tree[p<<1].l+1)*tree[p].tg;
tree[p<<1|1].val+=(tree[p<<1|1].r-tree[p<<1|1].l+1)*tree[p].tg;
tree[p<<1].tg+=tree[p].tg;
tree[p<<1|1].tg+=tree[p].tg;
tree[p].tg=0;
}
return;
}
void update(int l,int r,ll val,int p=1)
{
if(l<=tree[p].l && tree[p].r<=r)
{
tree[p].val+=(tree[p].r-tree[p].l+1)*val;
tree[p].tg+=val;
return;
}
spread(p);
int mid=tree[p].l+tree[p].r>>1;
if(l<=mid)
update(l,r,val,p<<1);
if(mid<r)
update(l,r,val,p<<1|1);
tree[p].val=tree[p<<1].val+tree[p<<1|1].val;
return;
}
ll query(int l,int r,int p=1)
{
if(l<=tree[p].l && tree[p].r<=r)
{
return tree[p].val;
}
spread(p);
int mid=tree[p].l+tree[p].r>>1;
ll ans=0;
if(l<=mid)
ans+=query(l,r,p<<1);
if(mid<r)
ans+=query(l,r,p<<1|1);
return ans;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>a[i];
build();
for(int i=1;i<=m;i++)
{
int op,l,r;
cin>>op>>l>>r;
if(op==1)
{
ll k;
cin>>k;
update(l,r,k);
}
else
{
cout<<query(l,r)<<endl;
}
}
return 0;
}