……线段树可是一个高端的东西。
一、啥是线段树
信息迅速发端的现在,人们已经发明出一个又一个算法以及数据结构。当人们嫌人眼校对会有错误,又会引起眼神疲劳时,人们发明了KMP;当人们嫌前缀和麻烦,效率不够,不能修改时,人们想到了树状数组;当人么又嫌树状数组只能单点修改不能区间修改时,人们,人们,人们,又想到了————
线 段 树 线段树 线段树
没错,线段树就是一个支持区间修改和区间查询的一个高级数据结构,在修改和查询中复杂度已经超过了分块成功的到达了 l o g ( n ) log(n) log(n)的级别,算是非常快了。
二、线段树的思想
线段树的思想类似于分治,就是不停地将一个区间分成一个又一个小区间,类似于分块,但显然更优。
例如对于一个 [ 1 , 10 ] [1,10] [1,10]的区间,他的图如下:
虽然有点模糊,不过大体是能够看清楚的。
他分区间的过程类似如下:
[
1
,
10
]
(
m
i
d
=
5
)
−
>
[
1
,
5
]
&
&
[
6
,
10
]
[1,10](mid=5)\ ->\ [1,5]\ \&\&\ [6,10]
[1,10](mid=5) −> [1,5] && [6,10]
[
6
,
10
]
(
m
i
d
=
8
)
−
>
[
6
,
8
]
&
&
[
9
,
10
]
[6,10](mid=8)\ ->\ [6,8]\ \&\&\ [9,10]
[6,10](mid=8) −> [6,8] && [9,10]
[
1
,
5
]
(
m
i
d
=
3
)
−
>
[
1
,
3
]
&
&
[
4
,
5
]
[1,5](mid=3)\ ->\ [1,3]\ \&\&\ [4,5]
[1,5](mid=3) −> [1,3] && [4,5]
[
6
,
8
]
(
m
i
d
=
7
)
−
>
[
6
,
7
]
&
&
[
8
,
8
]
[6,8](mid=7)\ ->\ [6,7]\ \&\&\ [8,8]
[6,8](mid=7) −> [6,7] && [8,8]
…
…
…
…
…
…
…
…
…
…
…
…
…
…
…
…
…
…
………………………………………………
………………………………………………
就是一直递归,二分的过程。
同时,它每一个点代表的都是一个区间,也能代表一个区间的和以及极值,这也为我们求区间极值或者区间和提供了一个方法。(balabla)
三、线段树的基本操作
一、建树(可能不需要)
如果是建树,题目中一般会给点权或者边权,有了这些基础的数值,就有了建树的内容。
我们以这题为例。
题目给了每个点的点权,同时也要求我们求区间内的数值和,也就是边权和,所以我们在建图时就可以把初始的区间的权值和给求好,这样我们求修改后的数值就会方便多了。
建图不难建,只需要我们用搜索建图即可。
Code
LL build(int x,int l,int r){
if (l==r) return tr[x].sum=num[l];//单独的权值
int mid=l+r>>1;
tr[x].sum+=build(x<<1,l,mid)+build(x<<1|1,mid+1,r);//将区间和累加上
return tr[x].sum;
}
当然也有求最大值的,只需要将代码稍微改变一下即可
Code
LL build(int x,int l,int r){
if (l==r) return tr[x].Max=num[l];//单独的权值
int mid=l+r>>1;
tr[x].Max=max(build(x<<1,l,mid),build(x<<1|1,mid+1,r));//将区间和累加上
return tr[x].sum;
}
二、区间修改
区间修改有点麻烦,一般都是在
[
x
,
y
]
[x,y]
[x,y]区间内每个 元素加上
k
k
k。
还是这张图为例:
- 假设我们要改变 [ 1 , 3 ] [1,3] [1,3]的值,我们就可以直接在 [ 1 , 3 ] [1,3] [1,3]出进行修改
- 假设我们要改变 [ 1 , 7 ] [1,7] [1,7]的值,那么我们就需要改变 [ 1 , 5 ] [1,5] [1,5]的值和 [ 6 , 7 ] [6,7] [6,7]的值。
这是我们就需要用一个
l
a
z
y
t
a
g
(
懒
标
记
)
lazy\ tag(懒标记)
lazy tag(懒标记)
为什么说是懒标记呢?那是因为一般的改变都是直接对原值进行 改变,但是lazy tag确实对一个区间加上一个需要改变的值。
什么意思呢?
我们假设要在
[
1
,
3
]
[1,3]
[1,3]加上一个值3
一般的程序是直接将三个数直接加上3,但是lazy tag确实在
[
1
,
3
]
[1,3]
[1,3]节点上加上一个3的标记,表示当前点所包含的区间需要加上数值3,有了这个标记之后,我们就可以在递归的时候顺便将值待下去,不需要多耗费时间,时间自然也节省了。
那么还是以这题的修改为例
Code
void change(int x,int l,int r){
if (l>R || r<L) return;
if (l>=L&&r<=R){
tr[x].tag+=k;//tag加上k
tr[x].sum+=(r-l+1)*k;//区间加上那么多
return;
}
int ls=x<<1,rs=x<<1|1,mid=l+r>>1;
if (tr[x].tag){
tr[ls].tag+=tr[x].tag;
tr[rs].tag+=tr[x].tag;
tr[ls].sum+=(mid-l+1)*tr[x].tag;
tr[rs].sum+=(r-mid)*tr[x].tag;
tr[x].tag=0;//别忘了赋0
}//对子区间进行改变
change(ls,l,mid);
change(rs,mid+1,r);
tr[x].sum=tr[ls].sum+tr[rs].sum;/返回的时候重新累加上
return;
}
三、区间查询
区间查询挺好做,仍然是递归。
只不过在求和时还需要在进行一下
p
u
s
h
d
o
w
n
(
对
子
区
间
进
行
改
变
)
pushdown\ (对子区间进行改变)
pushdown (对子区间进行改变)操作,因为在
c
h
a
n
g
e
change
change时很可能因为查询到了该区间而并没有继续往下改变,在查询时就会出问题,所以我们就要进行一次操作。其他的就按照dfs返回值即可。
那么仍然是这题
Code
LL findsum(int x,int l,int r){
if (l>R || r<L) return 0;
if (l>=L && r<=R) return tr[x].sum;//如果包含区间,直接返回
int ls=x<<1,rs=x<<1|1,mid=l+r>>1;
if (tr[x].tag){
tr[ls].tag+=tr[x].tag;
tr[rs].tag+=tr[x].tag;
tr[ls].sum+=(mid-l+1)*tr[x].tag;
tr[rs].sum+=(r-mid)*tr[x].tag;
tr[x].tag=0;
}//修改
return findsum(ls,l,mid)+findsum(rs,mid+1,r);//直接返回
}
例题精讲
因为我目前仍然没有做多少线段树的题目,所以还是只能这题了。。
那么将之前的三个代码合并一下就变成了完整的代码
Code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
struct node{
LL sum,tag;
}tr[400010];
LL num[400010];
LL k;
int n,m,L,R;
LL build(int x,int l,int r){
if (l==r) return tr[x].sum=num[l];
int mid=l+r>>1;
tr[x].sum+=build(x<<1,l,mid)+build(x<<1|1,mid+1,r);
return tr[x].sum;
}
void change(int x,int l,int r){
if (l>R || r<L) return;
if (l>=L&&r<=R){
tr[x].tag+=k;
tr[x].sum+=(r-l+1)*k;
return;
}
int ls=x<<1,rs=x<<1|1,mid=l+r>>1;
if (tr[x].tag){
tr[ls].tag+=tr[x].tag;
tr[rs].tag+=tr[x].tag;
tr[ls].sum+=(mid-l+1)*tr[x].tag;
tr[rs].sum+=(r-mid)*tr[x].tag;
tr[x].tag=0;
}
change(ls,l,mid);
change(rs,mid+1,r);
tr[x].sum=tr[ls].sum+tr[rs].sum;
return;
}
LL findsum(int x,int l,int r){
if (l>R || r<L) return 0;
if (l>=L && r<=R) return tr[x].sum;
int ls=x<<1,rs=x<<1|1,mid=l+r>>1;
if (tr[x].tag){
tr[ls].tag+=tr[x].tag;
tr[rs].tag+=tr[x].tag;
tr[ls].sum+=(mid-l+1)*tr[x].tag;
tr[rs].sum+=(r-mid)*tr[x].tag;
tr[x].tag=0;
}
return findsum(ls,l,mid)+findsum(rs,mid+1,r);
}
int main(){
scanf("%d %d",&n,&m);
for (int i=1;i<=n;i++) scanf("%lld",&num[i]);
build(1,1,n);
for (int i=1,o;i<=m;i++){
scanf("%d %d %d",&o,&L,&R);
if (o==1) scanf("%lld",&k),change(1,1,n);
else printf("%lld\n",findsum(1,1,n));
}
// for (int i=1;i<=9;i++)
// cout<<tr[i].sum<<endl;
return 0;
}