线段树

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数加上x

2.求出某区间每一个数的和

输入输出格式

输入格式:

 

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含3或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和

 

输出格式:

 

输出包含若干行整数,即为所有操作2的结果。

 

输入输出样例

输入样例#1: 复制

5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4

输出样例#1: 复制

11
8
20

说明

时空限制:1000ms,128M

数据规模:

对于30%的数据:N<=8,M<=10

对于70%的数据:N<=1000,M<=10000

对于100%的数据:N<=100000,M<=100000

(数据已经过加强^_^,保证在int64/long long数据范围内)

样例说明:


这是一个模板题,相信来搜这个题的coder都是刚入门线段树的,我也是刚学线段树,通过今天的学习,这里以我的思路来讲一下:

给你一串数字,对这串数组的多个区间进行操作,然后输出某一个区间的和或者积

首先我们来理解一下线段树的定义:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。

一般都会用数组来存储树状结构,成为树状数组,很少会用到链表,我们定义一个数组tree[10],表明这个树共有10个节点,默认从a[1]开始:

  1. 用a[1]当作根节点,
  2. a[2],a[3]分别为a[1]的左孩子和右孩子节点
  3. a[4],a[5]为a[2]的左孩子和右孩子节点、a[6],a[7]为a[3]的左孩子和右孩子节点
  4. .............一直到a[10]

like this:

可见a[i]的左孩子结点为a[2*i],右孩子结点为a[2*i+1],由于节点都是轮着从左到右放的,放满了就放到下一层,所以中间不会空着一个位置出来,这也是完全二叉树的性质。

好了,现在我们明白了树是怎么存储的了,那么看我们来看线段树是怎么存储的:

此时就要引用结构体了:

l,r表示题目中数组[l,r]的区间,sum表示[l,r]中所有元素的和,也就是区间和,lazy是一个标记,标记这个节点中[l,r]是否要进行某个操作,之所以要用long long 是因为区间和会很大,而lazy要和区间中元素的个数相乘,然后赋值给sum,所以尽量用一样的数据类型;

like this

头节点中,l和r表示了区间[1,10],如果lazy有一个值(不为0)就表示区间[1,10]中所有的值都要加上一个lazy,而sum表示这个区间中l-r+1个数的和。如果lazy为0,就表示不需要执行什么操作。

其他节点也是一样的原理,叶子结点有一个标识就是l==r,因为叶子节点只表示一个元素,我们可以利用这个特点来分别叶子节点。

为什么要用线段树,而不用普通的for循环来操作,因为具体到区间我们直接可以对节点中的区间操作,不用一个一个元素累加,时间复杂度会大大降低,至于线段树结点的个数为什么是4*max,假如题目给你n个结点,最坏情况下,最下面一层有n个结点,倒数第二层有n/2个结点,倒数第三层有n/4个结点。。。。等比数列求和取极限就是4*n个结点了

理解了这些概念后,就可以进行解题了,

假如题目给你10个数,我随机弄了几个数,我们将这串数字标号也就是数组的下标1~n,

注意是闭区间:

然后通过“建树”操作,把a[]数组里的树放到tree[]中的,通过递归与回溯:

void build(int x,int l,int r)
{
	tree[x].l=l;//区间赋值 
	tree[x].r=r;
	
	if(l==r) //如果是叶子节点 
	{ tree[x].sum=a[l]; return ;}
	 
	int mid=(l+r)/2;//二分 
	build(2*x,l,mid);//左孩子递归 
	build(2*x+1,mid+1,r);//右孩子递归 
	
	tree[x].sum=tree[2*x].sum+tree[2*x+1].sum;//回溯 
}

这个就需要一点点的抽象的思维了,学过搜索的的很快就会理解,由于递归是向下递归,回溯是向上回溯,我们需要将十个数放到叶子节点中,然后回溯到上面的结点,同时sum的值也会计算出来。

按照上面介绍的规律,左孩子与右孩子分别是2*i和2*1+1:

此时树建好了,如果要对某一个区间的所有元素进行操作的话,我们需要区间跟新,暂定为[x,y]中的元素要加q,区间中的元素从第向上的结点中的sum的值都会改变,此时我们引进lazy的概念,当向下递归时,发现结点中的区间被[x,y]包含,我们就把这个结点的lazy标记为q,从这个结点开始就不再向下递归,下一次递归的时候顺带把lazy像孩子结点传递

比如我们递归到第七个结点tree[7],发现这个结点所表示的区间[4,5]被[x,y]包含了,我们就进行tree[7].lazy=q;查询的时候直接返回 区间和加上lazy乘以元素个数,也就是 tree[7].sum+tree[7].lazy*(tree[7].r - tree[7].l +1),不需要再往下对一个一个的对元素进行操作了,是不是快很多,

void update(int x,int m,int n,int q)
{//x表示第几个结点,m,n表示操作区间,q表示区间中的每一个元素都要加q

	if(m<=tree[x].l&&tree[x].r<=n)//如果是节点中的区间被包含 
	{
		tree[x].sum+=(lon)q*(tree[x].r-tree[x].l+1);//区间和更新 
		tree[x].lazy+=q;//打上标记 
		return ;//停止这一层递归
	}
	
	if(tree[x].lazy!=0)//向下传递 
	{
		//如果父节点的lazy不为零的话,那么表示父节点被包含了,两个孩子节点 也一定会被包含 
		tree[2*x].sum+=tree[x].lazy*(tree[2*x].r-tree[2*x].l+1);// 左孩子区间和更新 
		tree[2*x].lazy+=tree[x].lazy;//左孩子打上懒标记 
		tree[2*x+1].sum+=tree[x].lazy*(tree[2*x+1].r-tree[2*x+1].l+1);//同理 
		tree[2*x+1].lazy+=tree[x].lazy;
		tree[x].lazy=0;//父节点lazy标记为零 
	}
	
	int mid=(tree[x].r+tree[x].l)/2;
	if(m<=mid) update(2*x,m,n,q);//如果区间左边有一部分被包含,就会递归传递标记 
	if(n>mid) update(2*x+1,m,n,q);//同理
	tree[x].sum=tree[x*2].sum+tree[x*2+1].sum;//回溯 
}

到这一步,就差查询了,由于更新的时候,不一定所有的标记都会向下传递,比如我要操作的区间是[4,8],而此时我递归到下面这个结点

这个结点的区间为[6,8],发现包含,lazy已经标记并停止递归,此时左右孩子都没有更新,

所以我们需要利用一切往下递归的过程,把lazy传递下去,如果递归次数无限多的的话,那么每一个元素都已经被安排了,这时,无论你想查询哪段区间我都可以给出来

查询函数:

lon find(int x,int l,int r)//第x个结点,查询区间 
{
	if(l<=tree[x].l&&tree[x].r<=r)
	//如果区间包含,直接加上sum,然后return 
	return tree[x].sum;
		
	if(tree[x].lazy!=0)//利用递归 
	{
		tree[2*x].sum+=tree[x].lazy*(tree[2*x].r-tree[2*x].l+1);
		tree[2*x].lazy+=tree[x].lazy;
		tree[2*x+1].sum+=tree[x].lazy*(tree[2*x+1].r-tree[2*x+1].l+1);
		tree[2*x+1].lazy+=tree[x].lazy;
		tree[x].lazy=0;
	}
	
	int mid=(tree[x].l+tree[x].r)/2;
	lon ans=0;
	if(l<=mid) ans+=find(2*x,l,r);//如果左结点的区间有一部分被包含 
	if(r>mid) ans+=find(2*x+1,l,r);
	return ans;
}

这个函数很好理解,如果我要查询的区间[x,y]包含[l,r] 那我就直接加上这个结点的sum,返回,如果左边有一部分被包含,就是半包含半不包含,我就往下递归,直到这个结点完全被包含为止,反正递归的尽头是叶子结点,一定会被包含的

这就是整个过程了,放一个模板传送门:https://www.cnblogs.com/AC-King/p/7789013.html

放上整个代码:(题解)

#include<bits/stdc++.h>
using namespace std;
using lon = long long;
struct point 
{
	int l,r;
	long long sum,lazy;
};
point tree[400020];
int a[100005];

void pushdown(int x)
{
	if(tree[x].lazy!=0)//向下传递 
	{
		//如果父节点的lazy不为零的话,那么表示父节点被包含了,两个孩子节点 也一定会被包含
		tree[2*x].sum+=tree[x].lazy*(tree[2*x].r-tree[2*x].l+1);// 左孩子区间和更新 
		tree[2*x].lazy+=tree[x].lazy;//左孩子打上懒标记 
		tree[2*x+1].sum+=tree[x].lazy*(tree[2*x+1].r-tree[2*x+1].l+1);//同理 
		tree[2*x+1].lazy+=tree[x].lazy;
		tree[x].lazy=0;//父节点lazy标记为零 
	}
}

void build(int x,int l,int r)
{
	tree[x].l=l;//区间赋值 
	tree[x].r=r;
	
	if(l==r) //如果是叶子节点 
	{ tree[x].sum=a[l]; return ;}
	 
	int mid=(l+r)/2;//二分 
	build(2*x,l,mid);//左孩子递归 
	build(2*x+1,mid+1,r);//右孩子递归 
	
	tree[x].sum=tree[2*x].sum+tree[2*x+1].sum;//回溯 
}


void update(int x,int m,int n,int q)
{
	if(m<=tree[x].l&&tree[x].r<=n)//如果是节点中的区间被包含 
	{
		tree[x].sum+=(lon)q*(tree[x].r-tree[x].l+1);//区间和更新 
		tree[x].lazy+=q;//打上标记 
		return ;
	}
	
	pushdown(x);
	
	int mid=(tree[x].r+tree[x].l)/2;
	if( m<=mid ) update(2*x,m,n,q);//如果区间左边有一部分被包含,就会标记传递 
	if( n>mid ) update(2*x+1,m,n,q);//同理
	
	tree[x].sum=tree[x*2].sum+tree[x*2+1].sum;//回溯 
}
lon find(int x,int l,int r)
{//第x个结点,查询区间 
	if(l<=tree[x].l&&tree[x].r<=r)	return tree[x].sum;
	//如果区间包含,直接加上sum,然后return 
		
	pushdown(x);
	
	int mid=(tree[x].l + tree[x].r) / 2;
	lon ans=0;
	if(l <= mid) ans += find(2*x,l,r);//如果区间左边有一部分被包含 
	if(r > mid) ans += find(2*x+1,l,r);
	return ans;
}
int main()
{
	ios::sync_with_stdio(false);
	int n,m,order,x,y,k;
	
	cin>>n>>m;
	
	for(int i=1;i<=n;i++)
	cin>>a[i];
	
	build(1,1,n);
	
	while(m--)
	{
		cin>>order;
		if(order==1)
		{
			cin>>x>>y>>k;
			update(1,x,y,k);
		}
		else 
		{
			cin>>x>>y;
			cout<<find(1,x,y)<<endl;
		}
	}
	return 0;
}

加深理解QAQ。。。。

 

 

 

 

 

 

 

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值