题目描述
如题,已知一个数列,你需要进行下面两种操作:
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]开始:
- 用a[1]当作根节点,
- a[2],a[3]分别为a[1]的左孩子和右孩子节点
- a[4],a[5]为a[2]的左孩子和右孩子节点、a[6],a[7]为a[3]的左孩子和右孩子节点
- .............一直到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。。。。