【数据结构-01】基本线段树
〇、前言
这是数据结构专栏的第一期,以后将会有一些讲解数据结构的博客。
希望这篇博客能帮到大家!
一、定义
什么是线段树?经常会听巨佬讲到。
线段树是一棵树,一棵二叉树。每个节点都表示一个区间,存储这个区间里的某个特定值(比如最大值,最小值)。
比如这个图:
节点里的数字表示左端点和右端点。
那么,我们定义结构体时,应该这样定义:
struct node{
int l,r,t;//l和r是两个端点,t表示存的特定值。
}a[400005];
为了方便,我们可以做这样一件事:
#define ls(x) (x<<1)
#define rs(x) ((x<<1)|1)
这样,我们访问左右孩子就会很方便了。
二、建树
如果我们要用 \(t\) 值存区间的最小值,我们就可以先想想怎么得到一个点的 \(t\) 值。
分情况讨论:
- 如果这个点是叶子节点,那么 \(t\) 值就是这个节点对应数组中这个位置(叶子节点只表示一个点)的最初的值。
- 否则,我们计算 \(fa.t=\min(lson.t,rson.t)\),从中间分开的两个区间中的两个最小值再取最小值,就是这个区间的最小值。
还有一个问题,那就是怎么决定两个子节点的中间线。为了平均,我们会选择定义 \(mid\) ,在左边的属于左孩子,右边的属于右孩子:
#define mid ((a[p].l+a[p].r)>>1)
这样的话,我们就选择用递归的方式,可以写出这样一个代码:
inline void pushup(int p){//就是上面的式子
a[p].t=min(a[ls(p)].t,a[rs(p)].t);
}
void build(int l,int r,int p){//建树,l和r表示左右端点,p是现在的根
a[p].l=l,a[p].r=r;//设置两端
if(l==r){//叶子节点
a[p].t=t[l];//t[l]是初值的数组
return;
}
build(l,mid,ls(p));//左孩子
build(mid+1,r,rs(p));//右孩子
pushup(p);//计算t值
}
基本的建树就完成了。
三、单点修改
当我们要让数组中的某个位置的数增加 \(c\)。怎么做呢?
我们想到,可以用线段树找到那个点,让他的 \(t\) 值增加 \(c\)。
我们递归找到对应的叶子节点,再修改。
代码:
void single_add(int x,int p,int c){//x加c,目前根为p
if(a[p].l==a[p].r&&a[p].l==x){//叶子节点而且就是这个点
a[p].t+=c;//修改
return;
}
if(x<=mid) single_add(x,ls(p),c);//在左边
else single_add(x,rs(p),c);//在右边
pushup(p);//更改了子节点,自然也要更新父节点
}
有人可能说,这个时间复杂度都是 \(O(\log_2 n)\) 的,还不如数组直接修改了呢,有什么意义呢?
别急,好戏还在后面呢。
四、区间修改
我们现在改了,要让数组中的一个区间的数都增加 \(c\)。又怎么办呢?
这个时候我们要请出一大法宝:
当我们要加的区间包含某个节点中的整个区间时,我们就不需要再往下加,直接让他的懒标记加上就可以了。因此,在结构体的定义中,需要加上他。
那么,当我们查询或分开加的时候需要分开这个区间,我们就要把标记下放,移动到两个儿子上。怎么移动呢?
inline void pushdown(int p){//下放
a[ls(p)].t+=a[p].lazy;//两个t值都增加了
a[rs(p)].t+=a[p].lazy;
a[ls(p)].lazy+=a[p].lazy;//为后面的下放做准备
a[rs(p)].lazy+=a[p].lazy;
a[p].lazy=0;//既然都下放了,就可以清空了
}
那么,我们修改的步骤基本上就清楚了:
void section_add(int l,int r,int p,int c){//l~r的区间内加c,目前根为p
if(l<=a[p].l&&a[p].r<=r){//所在的区间完全被罩住了
a[p].t+=c;//直接加上
a[p].lazy+=c;
return;
}
pushdown(p);//既然要分开,就要先下放
if(l<=mid) section_add(l,r,ls(p));//如果包含左边,就下放
if(r>mid) section_add(l,r,rs(p));//如果包含右边,就下放
pushup(p);//最后,再上传一遍
}
五、单点查询
和前面的单点修改差不多,就不详细讲了。
int single_query(int x,int p){//查找x,目前根为p
if(a[p].l==x) return a[p].t;//找到啦
pushdown(p);//要分开,先下放
if(x<=mid) return single_query(x,ls(p));//在左边
else return single_query(x,rs(p));//在右边
}
六、区间查询
如果查询的时候完全包含这个区间,那就直接返回。
如果完全不包含,就返回不影响结果的任意值。
其他情况,就是部分包含,就取左右孩子得到结果的最小值。
总体和区间修改类似。
int section_query(int l,int r,int p){//l~r的区间查找,目前根为p
if(l<=a[p].l&&a[p].r<=r) return a[p].t;//完全包含
if(a[p].l>r||a[p].r<l) return 2e9;//没找到,为不影响结果应该设的大一些
pushdown(p);//先下放,再找
return min(section_query(l,r,ls(p)),section_query(l,r,rs(p)));//计算结果
}
七、总结
其实线段树总体的代码都是差不多的,写多了就会发现。
另附一份完整代码(包含上面四种函数):
#include<bits/stdc++.h>
using namespace std;
#define ls(x) (x<<1)
#define rs(x) ((x<<1)|1)
#define mid ((a[p].l+a[p].r)>>1)
struct node{
int l,r,t,lazy;
}a[400005];
int t[400005];
inline void pushup(int p){//因题而异
a[p].t=min(a[ls(p)].t,a[rs(p)].t);
}
inline void pushdown(int p){//同上
a[ls(p)].t+=a[p].lazy;
a[rs(p)].t+=a[p].lazy;
a[ls(p)].lazy+=a[p].lazy;
a[rs(p)].lazy+=a[p].lazy;
a[p].lazy=0;
}
void build(int l,int r,int p){//建树
a[p].l=l,a[p].r=r;a[p].lazy=0;
if(l==r){
a[p].t=t[l];
return;
}
build(l,mid,ls(p));
build(mid+1,r,rs(p));
pushup(p);
}
void section_add(int l,int r,int p,int c){//l~r的区间内加c,目前根为p
if(l<=a[p].l&&a[p].r<=r){
a[p].t+=c;
a[p].lazy+=c;
return;
}
pushdown(p);
if(l<=mid) section_add(l,r,ls(p));
if(r>mid) section_add(l,r,rs(p));
pushup(p);
}
void single_add(int x,int p,int c){//x加c,目前根为p
if(a[p].l==a[p].r&&a[p].l==x){
a[p].t+=c;
return;
}
if(x<=mid) single_add(x,ls(p),c);
else single_add(x,rs(p),c);
pushup(p);
}
int section_query(int l,int r,int p){//l~r的区间查找,目前根为p
if(l<=a[p].l&&a[p].r<=r) return a[p].t;
if(a[p].l>r||a[p].r<l) return 2e9;//这里要根据题目的要求改变初始值
pushdown(p);
return min(section_query(l,r,ls(p)),section_query(l,r,rs(p)));//这里的函数要和pushup一样
}
int single_query(int x,int p){//查找x,目前根为p
if(a[p].l==x) return a[p].t;
pushdown(p);
if(x<=mid) return single_query(x,ls(p));
else return single_query(x,rs(p));
}
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>t[i];
build(1,n,1);
string op;
int x,y,c;
for(int i=1;i<=m;i++){
cin>>op>>x;
if(op=="section_add"){
cin>>y>>c;
section_add(x,y,1,c);
}else if(op=="single_add"){
cin>>c;
single_add(x,1,c);
}else if(op=="section_query"){
cin>>y;
cout<<section_query(x,y,1)<<endl;
}else if(op=="single_query"){
cout<<single_query(x,1)<<endl;
}
}
return 0;
}