题目描述
如题,已知一个数列,你需要进行下面两种操作:
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数据范围内)
样例说明:
思路
让我们从这道板子题开始,详解线段树入门。
目录
1.1 概述/前言
1.2 实现线段树
1.3 模板
1.1 概述/前言
现在有3道题。
1.题目描述:有n个数和一个序列a(范围1~n)。求在a[i]中某些区间在范围数的和。(n<=10^6)
A.pro:这他喵的还不简单,前缀和,搞定。
2.题目描述:有n个数,一个序列a(范围1~n),以及k个操作。在区间[l,r]全部内加一个数,并且求区间[l,r]的和。(n<=10^4)
A.pro:emm好吧,前缀和肯定不行了。
前缀和:求和操作时间复杂度:O(1);区间修改时间复杂度:O(n)。这复杂度显然是我们不希望看到的。
3.题目描述:有n个数,一个序列a(范围1~n),以及k个操作。在区间[l,r]全部内加一个数,并且求区间[l,r]的和。(n<=10^6)
A.pro:艹。
所以,我们需要一种强大的数据结构,使得完美潇洒地完成题目3的问题。
其实可以引入分块的做法,即把数组分成若干个块求解问题。
线段树!!!
线段树,之所以称之为树,是因为它本身是二叉树。它本质上是把分块数组树形化。线段树,在各个节点保存一个区间,可以高效地解决区间修改问题。由于二叉结构的特性,它的每次操作都使得时间复杂度近似于O(logN)。由于是一棵二叉树,每个节点的信息都会被logN左右的节点记录。空间消耗较大,一般是4n。所以线段树是一个典型的空间换时间的数据结构。
下图是一个区间[1,10]的线段树。
当我们需要对一个区间进行操作时,同样要将其化成若干个小区间,这个就是分块思想。正如前面所说,线段树就是把分块思想树形化,对任何一个信息都可以达到logN的速度。(但其实常数较大)
对于每一个子节点而言,都表示整个序列中的一段子区间;对于每个叶子节点而言,都表示序列中的单个元素信息;子节点不断向自己的父亲节点传递信息,而父节点存储的信息则是他的每一个子节点信息的整合。
1.2 实现线段树
1.2.1 存储
由于线段树是一棵二叉树,所以存储结构完全可以借鉴堆那样存。
由于二叉树的自身特性,对于每个父亲节点的编号i,他的两个儿子的编号分别是2i和2i+1,所以我们考虑写两个O(1)的取儿子函数:(这里用的位运算)
inline ll int leftnode(ll int p) {return p<<1;}//左节点(左儿子)
inline ll int rightnode(ll int p) {return p<<1|1;}//右节点(右儿子)
相反,如果我们要找父亲节点,就可以把左儿子除以2就能得到。
1.2.2 建树
接下来我们考虑建立一棵线段树。
建树的操作就是不断二分,直到遍历到叶子节点。到了叶子节点后,可以干我们想维护的事情。比如求区间和,RMQ,RSQ等。之后我们再回溯上去,其他节点都得跟着更新。
inline void push_up(ll int node)//维护父子节点之间的逻辑关系(合并2个儿子节点)
{
tree[node]=tree[leftnode(node)]+tree[rightnode(node)];//区间和
//tree[node]=min(tree[leftnode(node)],tree[rightnode(node)]);//最小值
}
void build(ll int node,ll int start,ll int end)//建树,node是当前节点,start和end是范围(是指a数组的范围)
{//线段树自底向上回溯,所以线段树的叶子节点在会被赋值,如果左右区间相同(start==end),说明这是叶子节点
if(start==end)
{
tree[node]=a[start];//区间和
minx[node]=a[start];//区间最小值
maxx[node]=a[start];//区间最大值
return;
}
else
{
register ll int mid=(start+end)>>1;
build(leftnode(node),start,mid);//左子树
build(rightnode(node),mid+1,end);//右子树,把当前根节点的儿子分别当成新节点,继续建立线段树
push_up(node);//维护线段树(区间和)
//区间最小值与最大值代码类似
}
}
根据线段树的服务对象,就写出了push_up这一函数。push_up操作的目的是为了维护父子间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。
所以push_up是在整合2个子节点的信息。
所以这也就说明建树时应该递归建树,这样才能保存父亲节点和子节点之间的信息。在建树的时候我们就应该维护父子节点的逻辑关系。
1.2.3 区间修改
在这个模版中,是要求我们累加。所以我们可以仿照建树的思路,不断二分,如果修改的区间包括了当前区间,就回溯,否则继续二分。
inline void lazy_ad(ll int node,ll int start,ll int end,ll int value)
{
tag[node]+=value;//懒标记,后面会讲
tree[node]+=value*(end-start+1);//由于是这个区间统一改变,所以要加上元素个数次
}//被包括的区间进行操作
void update(ll int node,ll int start,ll int end,ll int cl,ll int cr,ll int value)//区间修改,node是当前节点,start和end是范围(是指a数组的范围),cl和cr是要修改的区间,value是改成哪个数值
{//其实与建树类似,我们这题维护的修改操作是区间累计,二分+回溯,如果修改的区间包含了目前遍历的区间就回溯,否则二分
if(cl<=start && end<=cr)//如果修改区间包含当前区间
{
lazy_ad(node,start,end,value);//直接加
return;
}
else//如果不包含当前区间
{
register ll int mid=(start+end)>>1;
push_down(node,start,end,mid);//标记下传,后面会讲
if(cl<=mid)
{
update(leftnode(node),start,mid,cl,cr,value);
}
if(mid<cr)
{
update(rightnode(node),mid+1,end,cl,cr,value);
}
push_up(node);//维护线段树
}
}
1.2.4 区间查询
模板是让我们求区间和,那么就和区间修改的思路一样,不断二分,如果修改区间包括目前遍历的区间,那就返回这一区间的区间和,然后回溯。把询问到的叶子节点或被包含的区间相加就是答案。
ll int query(ll int node,ll int start,ll int end,ll int cl,ll int cr)//node是当前节点,start和end是范围(是指a数组的范围),L和R是在区间[L,R]里计算和
{//和区间修改一样,不断二分,如果修改区间包括当前区间,就返回这一区间的区间和,然后回溯。把访问到的叶子节点或被包含的区间相加就是答案
//cout<<"start="<<start<<endl;
//cout<<"end="<<end<<endl;
//cout<<endl;
if(start>=cl && end<=cr)//如果修改的区间包括当前遍历的区间
{
return tree[node];//返回这一区间的区间和
}
register ll int mid=(start+end)>>1,s(0);
push_down(node,start,end,mid);//标记下传
if(cl<=mid)
{
s=s+query(leftnode(node),start,mid,cl,cr);//把"s+"去掉也是等价的。因为s初值为0,我先遍历的是左子树,然后query是不断递归寻址,最后再给s的
}
if(mid<cr)
{
s=s+query(rightnode(node),mid+1,end,cl,cr);//左右子树元素累加
}
return s;//返回s总值
}
1.2.5 懒标记下传
其实还有永久化标记,那样的话就是主席树了(可持久化线段树)。
线段树的优点不在于全记录,那样的话就是O(n)了,显然不是我们希望的。而在于传递式记录。
标记下传(push_down)的本质和前面的操作时一样的,整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;如果四环以内只修改了自己的话,那就只改变自己。我们查询子节点的时候再进行更新,也就是我们不需要查询的时候就下传标记。再将使用的标记清0。
注意访问任何一个节点的时候,都需要保证该节点的祖先标记都被清空。
inline void push_down(ll int node,ll int start,ll int end,ll int mid)//标记下传,node是当前节点
{//线段树的优点不在于全记录,那样的话复杂度达到O(n);而是在与传递式记录
//本质和build,update之类的一样,如果操作一段区间,那只要记录在这段区间的公共祖先上(单点也是区间,长度为1)
//当我们需要查询子节点的信息时我们再更新,不需要查询的时候就打下传标记,要用的时候使用标记,再将使用的标记清0
//但是访问任何一个节点的时候,都要保证该节点的祖先节点已经被清空
//这,就是懒标记(lazy tag) ,简单粗暴省时间。
if(tag[node]==0)
{
return;
}
lazy_ad(leftnode(node),start,mid,tag[node]);
lazy_ad(rightnode(node),mid+1,end,tag[node]);
tag[node]=0;
}
1.3 模板
#include <stdio.h>
#include <iostream>
#define ll long long
#define maxn 100005
using namespace std;
ll int a[maxn],tree[maxn<<2],tag[maxn<<2],n,s,m;//a是序列,tree是区间和,tag是标记数组
ll int minx[maxn<<2],maxx[maxn<<2];//区间最小值和最大值,需要时会用到
inline ll int leftnode(ll int p) {return p<<1;}//左节点(左儿子)
inline ll int rightnode(ll int p) {return p<<1|1;}//右节点(右儿子)
inline void push_up(ll int node)//维护父子节点之间的逻辑关系(合并2个儿子节点)
{
tree[node]=tree[leftnode(node)]+tree[rightnode(node)];//区间和
//tree[node]=min(tree[leftnode(node)],tree[rightnode(node)]);//最小值
}
inline void lazy_ad(ll int node,ll int start,ll int end,ll int value)//区间累加
{
tag[node]+=value;//懒标记
tree[node]+=value*(end-start+1);//由于是这个区间统一改变,所以要加上元素个数次
}//被包括的区间进行操作
inline void push_down(ll int node,ll int start,ll int end,ll int mid)//标记下传,node是当前节点
{//线段树的优点不在于全记录,那样的话复杂度达到O(n);而是在与传递式记录
//本质和build,update之类的一样,如果操作一段区间,那只要记录在这段区间的公共祖先上(单点也是区间,长度为1)
//当我们需要查询子节点的信息时我们再更新,不需要查询的时候就打下传标记,要用的时候使用标记,再将使用的标记清0
//但是访问任何一个节点的时候,都要保证该节点的祖先节点已经被清空
//这,就是懒标记(lazy tag) ,简单粗暴省时间。
if(tag[node]==0)
{
return;
}
lazy_ad(leftnode(node),start,mid,tag[node]);
lazy_ad(rightnode(node),mid+1,end,tag[node]);
tag[node]=0;
}
void build(ll int node,ll int start,ll int end)//建树,node是当前节点,start和end是范围(是指a数组的范围)
{//线段树自底向上回溯,所以线段树的叶子节点在会被赋值,如果左右区间相同(start==end),说明这是叶子节点
if(start==end)
{
tree[node]=a[start];//区间和
minx[node]=a[start];//区间最小值
maxx[node]=a[start];//区间最大值
return;
}
else
{
register ll int mid=(start+end)>>1;
build(leftnode(node),start,mid);
build(rightnode(node),mid+1,end);//把当前根节点的儿子分别当成新节点,继续建立线段树
push_up(node);//维护线段树(区间和)
//区间最小值与最大值代码类似
}
}
void update(ll int node,ll int start,ll int end,ll int cl,ll int cr,ll int value)//区间修改,node是当前节点,start和end是范围(是指a数组的范围),cl和cr是要修改的区间,value是改成哪个数值
{//其实与建树类似,我们这题维护的修改操作是区间累计,二分+回溯,如果修改的区间包含了目前遍历的区间就回溯,否则二分
if(cl<=start && end<=cr)//如果修改区间包含当前区间
{
lazy_ad(node,start,end,value);//直接加
return;
}
else//如果不包含当前区间
{
register ll int mid=(start+end)>>1;
push_down(node,start,end,mid);//标记下传
if(cl<=mid)
{
update(leftnode(node),start,mid,cl,cr,value);
}
if(mid<cr)
{
update(rightnode(node),mid+1,end,cl,cr,value);
}
push_up(node);//维护线段树
}
}
ll int query(ll int node,ll int start,ll int end,ll int cl,ll int cr)//node是当前节点,start和end是范围(是指a数组的范围),L和R是在区间[L,R]里计算和
{//和区间修改一样,不断二分,如果修改区间包括当前区间,就返回这一区间的区间和,然后回溯。把访问到的叶子节点或被包含的区间相加就是答案
//cout<<"start="<<start<<endl;
//cout<<"end="<<end<<endl;
//cout<<endl;
if(start>=cl && end<=cr)//如果修改的区间包括当前遍历的区间
{
return tree[node];//返回这一区间的区间和
}
register ll int mid=(start+end)>>1,s(0);
push_down(node,start,end,mid);//标记下传
if(cl<=mid)
{
s=s+query(leftnode(node),start,mid,cl,cr);//把"s+"去掉也是等价的。因为s初值为0,我先遍历的是左子树,然后query是不断递归寻址,最后再给s的
}
if(mid<cr)
{
s=s+query(rightnode(node),mid+1,end,cl,cr);//左右子树元素累加
}
return s;//返回s总值
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
register ll int i;
cin>>n>>m;//有n个数,m个操作
for(i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,1,n);//我要建立一棵树,从1号节点出发,范围是a数组的1~n
//for(i=1;i<=15;i++)//查看有没有建树成功,父亲节点与两个儿子节点的关系是2n和2n+1(当从1开始计数时)。
//{
// cout<<"tree["<<i<<"]="<<tree[i]<<endl;
//}
//cout<<endl;
for(i=1;i<=m;i++)//处理m条操作
{
ll int f,start,end,k;
cin>>f>>start>>end;
if(f==1)
{
cin>>k;
update(1,1,n,start,end,k);
}
if(f==2)
{
cout<<query(1,1,n,start,end)<<endl;
}
}
return 0;
}