题目来源:https://www.luogu.org/problemnew/show/3372,侵删。
题目描述
如题,已知一个数列,你需要进行下面两种操作:
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数据范围内)
样例说明:
对于较小的数据,当然可以用数组模拟,代码简单人见人爱。但针对较大数据就会果断GG,所以需要使用线段树,线段树在处理较大数据时有奇效,时间复杂度为O(logn),但占用空间也相对较大,达到4n。
(1)建树
线段树的主要思想是二分,将一个大区间不停二分直到成为一个单点,如图:
在对区间进行操作时,只需要对大区间进行操作,不用下到每个叶结点,查询时同理。
建树时,我们将线段树储存为线性结构,又因为该线段树为完全二叉树结构,所以对于每一个父节点x[v],x[v×2]和x[v×2+1]为其子节点。而对于每一个元素,需要记录的变量有:左区间(le),右区间(ri)。此外,根据题意,还应记录该区间的元素和(sum)以及区间操作的加数(ad),所以定义结构体如下:
struct sd{
int le,ri;
long long sum,ad;
sd()
{
memset(this,0,sizeof(this));
}
};
其中,this指向该结构体中的元素。memset语句则将该结构体中的所有元素初始化为0。
记录加数(ad)的目的是延后对区间加法的运算,在查询时再调用。另外,该加数针对的是其子节点的加数,而非自己的。
然后就是建树操作了,将子节点的区间和合并到父节点上,如果该节点已经是最底层叶节点(即左右区间相等),那么就从读入的初始数据中赋值。建树时我们采用递归操作,每次都进行二分,遇到叶结点时回溯,合并区间。
注意:对二分点(mid)所属区间的划分,否则会莫名RE,在这里博主定义左区间为[le,mid],右区间为(mid,ri]。在后面进行区间加/查询时也要遵守建树时的定义。
代码如下:
void up(int v)
{
tree[v].sum=tree[v*2].sum+tree[v*2+1].sum;//合并自己子区间的和
}
void build(int v,int le,int ri)
{
tree[v].le=le;
tree[v].ri=ri;//确定区间范围
if(le==ri)
{
tree[v].sum=x[le];
return;//当递归到叶结点时,回溯
}
int mid=(le+ri)/2;
build(v*2,le,mid);
build(v*2+1,mid+1,ri);//递归二分
up(v);//合并子区间
}
在进行区间加的操作时,同样采用递归二分进行赋值,遇到被完全包含的区间时就停止递归,将该区间的区间和进行修改,然后将加数传递给子区间。
void pushdown(int t,int le,int ri)
{//将加数传递给子区间
int mid=(le+ri)/2;
tree[t*2].ad+=tree[t].ad;
tree[t*2+1].ad+=tree[t].ad;//传递加数
tree[t*2].sum+=(mid-le+1)*tree[t].ad;
tree[t*2+1].sum+=(ri-mid)*tree[t].ad;//修改区间和
tree[t].ad=0;//传递完毕后将父区间的加数归零
}
void add(int t,int le,int ri,int ll,int rr,int ad)
{
if(ll<=le&&ri<=rr)
{//如果遇到完全包含的区间,更改其加数与区间和后回溯
tree[t].ad+=ad;
tree[t].sum+=(ri-le+1)*ad;
return;
}
if(tree[t].ad!=0)
{//如果该区间有加数,将加数传递给子区间,为后面的二分递归做准备
pushdown(t,le,ri);
}
int mid=(le+ri)/2;
if(ll<=mid)
{//没有完全包含则二分递归继续进行区间加
add(t*2,le,mid,ll,rr,ad);
}
if(rr>mid)
{
add(t*2+1,mid+1,ri,ll,rr,ad);
}
up(t);//更改父节点
}
然后就是查询操作,同样递归二分。参数从左到右依次为:当前查询的节点编号,当前查询到的左、右区间以及要求查询的左、右区间。
long long qsum(int t,int le,int ri,int ll,int rr)
{
if(ll<=le&&ri<=rr)
{//如果该区间被完全包含,返回该区间的和
return tree[t].sum;
}
if(tree[t].ad!=0)
{//和区间加一样,更改子结点的值为二分递归查找做准备
pushdown(t,le,ri);
}
int mid=(le+ri)/2;long long s=0;
if(ll<=mid)
{//二分查找
s=qsum(t*2,le,mid,ll,rr);
}
if(mid<rr)
{
s+=qsum(t*2+1,mid+1,ri,ll,rr);
}
//返回左右两个区间查询得到的结果之和
return s;
}
基本的函数都已经介绍完毕,在调用函数进行区间加/查询时,需要将当前查询的节点编号(t),当前查询到的左(le)、右(ri)区间都设为最大的区间(即t=1,le=1,ri=n),剩下的都交给递归解决。
最后贴上博主丑陋的代码:
#include<cstdio>
#include<cstring>
using namespace std;
struct sd{
int le,ri;
long long sum,ad;
sd()
{
memset(this,0,sizeof(this));
}
};
long long x[100005];
sd tree[400005];
void up(int v)
{
tree[v].sum=tree[v*2].sum+tree[v*2+1].sum;
}
void build(int v,int le,int ri)
{//建树
tree[v].le=le;
tree[v].ri=ri;
if(le==ri)
{
tree[v].sum=x[le];
return;
}
int mid=(le+ri)/2;
build(v*2,le,mid);
build(v*2+1,mid+1,ri);
up(v);
}
void pushdown(int t,int le,int ri)
{
int mid=(le+ri)/2;
tree[t*2].ad+=tree[t].ad;
tree[t*2+1].ad+=tree[t].ad;
tree[t*2].sum+=(mid-le+1)*tree[t].ad;
tree[t*2+1].sum+=(ri-mid)*tree[t].ad;
tree[t].ad=0;
}
void add(int t,int le,int ri,int ll,int rr,int ad)
{//区间加
if(ll<=le&&ri<=rr)
{
tree[t].ad+=ad;
tree[t].sum+=(ri-le+1)*ad;
return;
}
if(tree[t].ad!=0)
{
pushdown(t,le,ri);
}
int mid=(le+ri)/2;
if(ll<=mid)
{
add(t*2,le,mid,ll,rr,ad);
}
if(rr>mid)
{
add(t*2+1,mid+1,ri,ll,rr,ad);
}
up(t);
}
long long qsum(int t,int le,int ri,int ll,int rr)
{//查询区间和
if(ll<=le&&ri<=rr)
{
return tree[t].sum;
}
if(tree[t].ad!=0)
{
pushdown(t,le,ri);
}
int mid=(le+ri)/2;long long s=0;
if(ll<=mid)
{
s=qsum(t*2,le,mid,ll,rr);
}
if(mid<rr)
{
s+=qsum(t*2+1,mid+1,ri,ll,rr);
}
return s;
}
int main()
{
int n,m;
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
scanf("%d",&x[i]);//读入初始数据
}
build(1,1,n);//建树
for(int i=1;i<=m;i++)
{
int p;
int x,y,k;
scanf("%d",&p);
if(p==1)
{
scanf("%d%d%d",&x,&y,&k);
add(1,1,n,x,y,k);//参数都设为最大区间
}
else
{
scanf("%d%d",&x,&y);
printf("%lld\n",qsum(1,1,n,x,y));//同上
}
}
return 0;
}
线段树作为一种高效的数据结构,需要4n的空间作为牺牲(ps:请一定把数组开的够大)但是相应的,面对更大的数据(>=5000000)时内存炸裂依然会GG,那么针对这种情况的优化以(lǎo)后(zi)再(bú)讲(huì)。另外,针对线段树的维护也是一件头疼的事,要维护的量往往不会像例题一样明显,同时也会更加复杂,需要靠大家自己的修为了。