引入
我们做题时,往往会看见一些关于区间求和的问题。
我们可以很轻松的打出下面的代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+100;
int n,num[N];
char op;
int l,r;
#define ll long long
ll tot[N];
int main(){
ios::sync_with_stdio(0);
cin>>n;
for(int i=1;i<=n;i++){
cin>>num[i];
tot[i]=tot[i-1]+num[i];
}
int q;
cin>>q;
while(q--){
cin>>l>>r;
cout<<tot[r]-tot[l-1]<<'\n';
}
return 0;
}
但是如果加上区间加呢,加上区间最大/最小值呢?
我们的时间复杂度就变成的
O
(
n
2
)
O(n^2)
O(n2) 了,非常的慢(每次需要更新 tot
数组)。
我们也会发现数据范围有时候非常大( n ≤ 1 0 6 n\le 10^6 n≤106)。
所以此时,我们
O
(
n
2
)
O(n^2)
O(n2) 级别的算法已经没有什么作用了,我们现在需要想出一种优化,让这个查询优化到
O
(
n
)
O(n)
O(n) 或者是
O
(
n
l
o
g
2
n
)
O(nlog_2n)
O(nlog2n) 级别的算法,否则我们是绝对 TLE
了。
线段树有什么作用
于是,我们高大上的线段树就出来了(其实也不高端的……)。
线段树的原理和实现
我们现在来交大家怎么快速理解线段树以及打出线段树的代码。
本节课我们先讲一下最基本的线段树,例题是
我们发现这道题目只需要维护区间和 和 区间加 即可。
其实线段树的重点只有两个字:递归
步骤一:递归算法来建树
首先,最重要的,我们必须先将线段树的轮廓建出来啊(当然有的题目不需要建树,那就跳过这一步吧)。
我们怎么建树呢?
我们先来看看线段树的大致样子:
我们发现,一个节点如果存储的区间是
[
i
,
i
]
[i,i]
[i,i]。那么这就是一个叶子节点,否则我们将这个区间劈成两半,然后分别再次递归就可以了。
我们就可以写出这个建树代码。
void build(int l,int r,int num){
//l为当前区间左处,r为当前区间右处,num为当前节点编号
if(l==r){//如果当前区间是[i,i]的形式
tree[num]=number[l];//那么我们直接赋值即可
return;
}
int mid=(l+r)>>1;
build(l,mid,num<<1);
build(mid+1,r,num<<1|1);
//num<<1|1 等价于 num*2+1
tree[num]=tree[num<<1]+tree[num<<1|1];
//我们将这个节点的值存储下来
return;
}
步骤二:lazy_tag来标记
我们将线段树的轮廓建好后,就可以来处理区间加的问题了。
我们再看一下这张图片:
我们还是可以写一个递归函数来处理这个问题。
但是,我们发现,我们每次在区间修改时,我们需要不停的递归求解,直到线段树的叶子节点。
这样效率有一点低。那么,我们如何提高这个东西的效率呢?
我们这时候就要使用我们的 lazy_tag
来维护了。
lazy_tag
就是当前节点需要被加时,我们先不加,而是打上一个标记,等到下次我们询问的时候再下放。
所以这时候,我们就需要两个函数分别是 实现区间加的函数 以及 实现 lazy_tag
下放的函数。
我们来看代码实现:
void push_down(ll l,ll r,ll num){
if(lazy_tag[num]){//如果当前节点有懒标记
ll mid=(l+r)>>1;
//懒标记下放
lazy_tag[num<<1]+=lazy_tag[num];
lazy_tag[num<<1|1]+=lazy_tag[num];
//数据总和增加
tree[num<<1]+=(lazy_tag[num]*(mid-l+1));
tree[num<<1|1]+=(lazy_tag[num]*(r-mid));
lazy_tag[num]=0;
}
return;
}
void add(ll want_l,ll want_r,ll l,ll r,ll num,ll add_num){
if(want_l<=l&&want_r>=r){//如果当前区间在你想要的范围之内
tree[num]+=(r-l+1)*add_num;//将区间总和增加
lazy_tag[num]+=add_num;//直接打上lazy_tag
return;
}
ll mid=(r+l)/2;//开始递归求解
push_down(l,r,num); //先下放懒标记
if(want_l<=mid)add(want_l,want_r,l,mid,num*2,add_num);//如果当前区间[l,mid]和我想要的区间有交集
if(want_r>mid)add(want_l,want_r,mid+1,r,num*2+1,add_num);//如果当前区间[mid+1,r]和我想要的区间有交集
tree[num]=tree[num*2]+tree[num*2+1];//最后储存
return;
}
这就是我们的区间加的操作的实现了。
步骤三:相加求解区间和
我们已经完成了建树,区间加的操作了。
我们接着来弄求解区间总和的操作。
看一张图片:
我们如果需要求解区间
[
8
,
12
]
[8,12]
[8,12] 的区间和,步骤如下:
首先,我们来到节点 1 1 1:发现 [ 8 , 12 ] [8,12] [8,12] 的区间并不是完全包含一的节点,于是开始递归向下求解。
然后,我们递归来到节点 2 2 2:发现 [ 8 , 12 ] [8,12] [8,12] 的区间并确实完全包含二的节点,此时返回节点二的值。
我们来到节点 3 3 3:发现 [ 8 , 12 ] [8,12] [8,12] 的区间与节点三所代表的区间有交集但并不是完全包含三的节点,于是开始递归向下求解。
我们来到节点 6 6 6:发现 [ 8 , 12 ] [8,12] [8,12] 的区间与节点三所代表的区间有交集但并不是完全包含六的节点,于是开始递归向下求解。
我们来到节点 12 12 12,发现此时正好是完全被区间 [ 8 , 12 ] [8,12] [8,12] 包含,所以此时返回节点 12 12 12 的值。
最后,我们将这些值的和全部加起来,就可以得到最后的答案。
我们上代码:
ll find_tot(ll want_l,ll want_r,ll l,ll r,ll num){
if(want_l<=l&&want_r>=r)return tree[num];
ll ans=0,mid=(r+l)/2;
push_down(l,r,num);
if(want_l<=mid)ans=find_tot(want_l,want_r,l,mid,num*2);
if(want_r>mid)ans+=find_tot(want_l,want_r,mid+1,r,num*2+1);
return ans;
}
那么,这道题目就可以直接 A C AC AC 了。
AC代码
我们的代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=1e5+100;
ll number[N],tree[N<<2];
ll n,op,x,y,k,m;
ll lazy_tag[N<<2];
void build(ll l,ll r,ll num){
if(l==r){
tree[num]=number[l];
return;
}
ll mid=(r+l)/2;
build(l,mid,num*2);
build(mid+1,r,num*2+1);
tree[num]=tree[num*2]+tree[num*2+1];
return;
}
void push_down(int l,int r,int num){
if(lazy_tag[num]){
int mid=(l+r)>>1;
lazy_tag[num<<1]+=lazy_tag[num];
lazy_tag[num<<1|1]+=lazy_tag[num];
tree[num<<1]+=(lazy_tag[num]*(mid-l+1));
tree[num<<1|1]+=(lazy_tag[num]*(r-mid));
lazy_tag[num]=0;
}
return;
}
void add(ll want_l,ll want_r,ll l,ll r,ll num,ll add_num){
if(want_l<=l&&want_r>=r){
tree[num]+=(r-l+1)*add_num;
lazy_tag[num]+=add_num;
return;
}
ll mid=(r+l)/2;
push_down(l,r,num);
if(want_l<=mid)add(want_l,want_r,l,mid,num*2,add_num);
if(want_r>mid)add(want_l,want_r,mid+1,r,num*2+1,add_num);
tree[num]=tree[num*2]+tree[num*2+1];
return;
}
ll find_tot(ll want_l,ll want_r,ll l,ll r,ll num){
if(want_l<=l&&want_r>=r)return tree[num];
ll ans=0,mid=(r+l)/2;
push_down(l,r,num);
if(want_l<=mid)ans=find_tot(want_l,want_r,l,mid,num*2);
if(want_r>mid)ans+=find_tot(want_l,want_r,mid+1,r,num*2+1);
return ans;
}
int main(){
scanf("%lld %lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&number[i]);
}
build(1,n,1);
for(int i=1;i<=m;i++){
scanf("%lld",&op);
if(op==1){
scanf("%lld %lld %lld",&x,&y,&k);
add(x,y,1,n,1,k);
}
else if(op==2){
scanf("%lld %lld",&x,&y);
printf("%lld\n",find_tot(x,y,1,n,1));
}
}
return 0;
}
那么,这次的学习就到这里了,下次再见!