【数据结构-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\)。又怎么办呢?
这个时候我们要请出一大法宝:

\[\Huge \texttt L\color{red}{\texttt{azytag}} \]

当我们要加的区间包含某个节点中的整个区间时,我们就不需要再往下加,直接让他的懒标记加上就可以了。因此,在结构体的定义中,需要加上他。
那么,当我们查询或分开加的时候需要分开这个区间,我们就要把标记下放,移动到两个儿子上。怎么移动呢?

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;
}
posted @ 2022-03-21 20:41  cyx001  阅读(96)  评论(8编辑  收藏  举报