一个线段树

发布于:2022-12-16 ⋅ 阅读:(259) ⋅ 点赞:(0)

线段树的本质,是一种加快运算速度的数据结构,将一串数字不断从中间分开的方式,知道分成一棵二叉树。可以将某区间需要求的答案保存在父节点上,再通过搜索二叉树求得答案。(据说)所有可有用树状数组解决的问题都可以用线段树解决,尤其是区间修改区间查询等操作,而且也不会慢太多。


一、用图看一下?

图中x,y表示x到y的一段区间,我们很容易看出来,是把1——10的一段序列拆成二叉树的形式了。

需要把下面子节点的信息,通过递归存储到父节点上,这样求取区间信息的时候就很方便。

当我们需要修改区间时(单点看做最下面的一段小区间,同理),在这个区间对应的根上打标记,

等到需要查询这个区间(单点)时,再向下查。因为是从大区间向小区间查,所以每次循环点的编

号找到的一定是符合条件的最大区间。

二、怎么整?

1.建树

1.建一个结构体,把一个点(root:点的编号)的各个信息都存下来。

struct tree{
	int ll,rr,anss,mark;
}a[40005];//需要开四倍空间!
//ll表示这个区间的左端点
//rr表示这个区间的右端点
//anss表示这一段区间的答案(范围为根下属子树)这里举例为这段区间的最大值
//mark表示标记过该点的修改值(如+1,mark=1,表示差值)

 2.通过递归,将一个已知的数组拆成二叉树。递归边界是l=r,易得此时的区间为(x,x),即递归到了单个数字,就可以返回了。

3.每次修改mid=(l+r)/ 2 ,向左右两边建这科树。

4.很容易观察得到左边节点为root*2,右边节点为root*2+1。

void build(int l,int r,int root){
	a[root].ll=l;
	a[root].rr=r;
	if(l==r){
		a[root].anss=b[l];
	} 
	int mid=(l+r)/2;
	build(l,mid,root*2);
	build(mid+1,r,root*2+1);
	a[root].anss=max(a[root*2].anss,a[root*2+1].anss);
}
//b[i]是输入数组,表示原序列 

 

 

2.更新数字(举例:把该点数字+1)

1.从第一个root开始搜(此时root=1),如果要修改的区间就在这个root的大区间中,就把这个root的值先进行修改(本例子中求最大值,所有数+1,最大值也+1,所以anss直接+1),并且打上标记(mark+1,意味着如果之后需要求解root的子区间的最大值,就需要在向下搜到答案区间时,加上沿途的mark)。

2.如果需要求解的区间不在这个root的区间中,就向下搜,并且加上沿途的mark(进行过这个操作时,所有数的mark就都进行了更新,维护了mark是下面子树要加的值这个性质),顺便更新anss。

void update(int l,int r,int root){
	if(l<=a[root].ll&&a[root].rr<=r){
        //因为是从大区间向小区间查,所以每次循环点的编号找到的一定是符合条件的最大区间。
		a[root].anss+=1;
		a[root].mark+=1;
	}
	else{
		godown(root);//这个函数待会儿说
		int mid=(a[root].ll+a[root].rr)/2;
		if(l<=mid)//说明左子树上有需要操作的区间
			update(l,r,root*2);
		if(r>mid)//同理
			update(l,r,root*2+1);
		a[root].anss=max(a[root*2].anss,a[root*2+1.anss]);	 
	}
}

3.打标记的维护函数godown

1.维护root的mark是它的子区间mark的和,子区间的anss需要加上父亲节点的mark。

2.为了避免向下搜索时出现断层,要每次手动操作将root的两个直接的儿子赋值。

3.使用完时候将root的mark赋值为0,避免重复加1。

void godown(int root){
	if(a[root].mark!=0){
		a[root*2].mark+=a[root].mark;
		a[root*2+1].mark+=a[root].mark;
		a[root*2].anss+=a[root].mark;
		a[root*2+1].anss+=a[root].mark;
		a[root].mark=0;
	}
} 

 

 

 

 

 4.求最大值

把之前的mark加上,求值

void maxx(int l,int r,int root){
	int mid=(a[root].ll+a[root].rr)/2;
	int anss=-0x3f3f3f3f;
	if(l<=a[root].ll&&s[root].rr<=r){
		//因为是从大区间向小区间查,所以每次循环点的编号找到的一定是符合条件的最大区间。
		return a[root].anss;
	}
	else{
		godown(root);//在求值前记得把mark上的值加上 
		if(l<=mid)ans=max(ans,maxx(l,r,root*2));
		if(r>mid)ams=max(ans,maxx(l,r,root*2+1));
		return ans; 
	}
}

 

 

 三.我们来看一下最终版?

题目描述

给定一个数列,初值全为0,现对这个序列有两种操作:

操作1:将第k1 个数 到 第k2 个数加1;

操作2:查询 从第k1个数到第k2个数得最大值。

输入格式

第一行给定一个整数n,表示有n个操作。

以下接着n行,每行三个整数,表示一个操作。

输出格式

若干行,查询一次,输出一次。

#include<bits/stdc++.h>
using namespace std;
int b[400005],n,k1,k2,s;
struct emm{
	int ll,rr,anss,mark;
}a[400005];
void build(int root,int l,int r){
	a[root].ll=l;
	a[root].rr=r;
	if(l==r){
		a[root].anss=b[l];
		return;
	}
	int mid=(l+r)/2;
	build(root*2,l,mid);
	build(root*2+1,mid+1,r);
	a[root].anss=max(a[root*2].anss,a[root*2+1].anss);	
}
void godown(int root){
	if(a[root].mark!=0){
		a[root*2].mark+=a[root].mark;
		a[root*2+1].mark+=a[root].mark;
		a[root*2].anss+=a[root].mark;
		a[root*2+1].anss+=a[root].mark;
		a[root].mark=0;
	}
}
void update(int root,int l,int r){
	if(l<=a[root].ll&&a[root].rr<=r){
		a[root].anss+=1;
		a[root].mark+=1;
		return;
	}
	else{
		godown(root);
		int mid=(a[root].ll+a[root].rr)/2;
		if(l<=mid)update(root*2,l,r);
		if(r>mid)update(root*2+1,l,r);
		a[root].anss=max(a[root*2].anss,a[root*2+1].anss);
	}
	
}
int maxx(int root,int l,int r){
	int ans=-0x3f3f3f3f;
	int mid=(a[root].ll+a[root].rr)/2;
	if(l<=a[root].ll&&a[root].rr<=r){
		return a[root].anss;
	}else{
		godown(root);
		if(l<=mid)ans=max(ans,maxx(root*2,l,r));
		if(r>mid)ans=max(ans,maxx(root*2+1,l,r));
		return ans;
	}
	
}
int main(){
	scanf("%d",&n);
	build(1,1,n); 
	for(int i=1;i<=n;i++){
		scanf("%d%d%d",&s,&k1,&k2);
		if(s==1)update(1,k1,k2);
		if(s==2)printf("%d\n",maxx(1,k1,k2));
	}
	return 0;
}

本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到