问题引入:
有一个长度为 n n n的数组 A n A_n An,对数组进行 m m m次操作,每次操作为下面两种之一:
1、将第 x x x个数加1
2、求 [ x , y ] [x,y] [x,y]的区间和
这个问题可以用暴力或者前缀和来解决,但是暴力求和复杂度为
O
(
n
)
O(n)
O(n),总复杂度为
O
(
m
n
)
O(mn)
O(mn),前缀和的修改复杂度为
O
(
n
)
O(n)
O(n),总复杂度依然为
O
(
m
n
)
O(mn)
O(mn),如果
m
m
m和
n
n
n在
1
0
5
10^{5}
105以上,无论是暴力算法还是前缀和都会超时,这时,我们就需要用线段树来解决这个问题
首先,使用线段树有一些要求,设我们对区间的运算为
⊕
⊕
⊕,总区间为
A
A
A,左子区间为
L
L
L,右子区间为
R
R
R,那么运用线段树的前提为:
⊕
A
=
(
⊕
L
)
⊕
(
⊕
R
)
⊕A=(⊕L)⊕(⊕R)
⊕A=(⊕L)⊕(⊕R)
满足条件的运算如
m
a
x
max
max、
m
i
n
min
min、
s
u
m
sum
sum
拿
m
a
x
max
max运算举例:
A
A
A区间的最大值等于
L
L
L区间的最大值和
R
R
R区间的最大值取
m
a
x
max
max运算,原因显然
满足这个条件的运算,就能用线段树优化
一、线段树建树
首先,在例子中,线段树需要维护区间左端点
l
l
l、区间右端点
r
r
r和区间和
s
u
m
sum
sum
建树的思路,先判断当前结点是否为叶结点,如果是,就直接将
l
、
r
、
s
u
m
l、r、sum
l、r、sum赋值,
l
和
r
l和r
l和r都是结点编号,sum就是
a
[
l
]
a[l]
a[l]即结点值。如果不是,将区间分成左右子区间,根据左右子区间计算原区间的值,定义
m
i
d
=
l
+
r
2
mid=\frac{l+r}{2}
mid=2l+r,左子区间为
[
l
,
m
i
d
]
[l,mid]
[l,mid],右子区间为
[
m
i
d
+
1
,
r
]
[mid+1,r]
[mid+1,r],设原区间的编号为
i
i
i左子区间和右子区间的编号分别为
2
i
2i
2i和
2
i
+
1
2i+1
2i+1
现在就可以写出线段树建树代码了
void build(long long i,long long l,long long r){
tr[i].l = l;
tr[i].r = r;
if (l == r){
tr[i].sum = a[l];
return ;
}
long long mid = (l + r) >> 1;
build(i * 2,l,mid);
build(i * 2 + 1,mid + 1,r);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum;
}
二、单点修改
比较简单,一路递归过去,如果把这个点包含了,就加上对应值,否则返回
void update(long long i,long long x){
if (tr[i].l > x || tr[i].r < x) return ;
if (tr[i].r <= x && tr[i].l >= x){
tr[i].sum++;
return ;
}
long long mid = (tr[i].l + tr[i].r) >> 1;
if (x <= mid) update(i * 2,x);
if (x > mid) update(i * 2 + 1,x);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum;
}
三、区间询问
区间询问和单点修改的思路是一样的,如果询问区间完全包含访问区间,就直接把答案加上,如果询问区间和访问区间一点重合都没有,就直接返回 0 0 0,如果有重合就递归访问区间的左右子区间,分别计算左右子区间对询问区间的贡献,加起来就是原区间的贡献了
long long query(long long i,long long l,long long r){
if (tr[i].l > r || tr[i].r < l) return 0;
if (tr[i].r <= r && tr[i].l >= l) return tr[i].sum;
long long mid = (tr[i].l + tr[i].r) >> 1;
long long ans = 0;
if (l <= mid) ans += query(i * 2,l,r);
if (r > mid) ans += query(i * 2 + 1,l,r);
return ans;
}
这样的话,最开始引入的例子就可以解决了
四、区间修改
如果将引入的例子改成将
[
l
,
r
]
[l,r]
[l,r]范围内所用的数加
1
1
1,那么采用原来的方法,将
[
l
,
r
]
[l,r]
[l,r]内所有的数挨个进行操作,时间复杂度就会退化成
O
(
m
n
l
o
g
2
n
)
O(mnlog_2n)
O(mnlog2n),还不如用暴力算法和前缀和算法,这个时候我们就要采用
l
a
z
y
t
a
g
lazytag
lazytag,即懒标记。懒标记是指暂时不更新所有的区间值,只是留需要更新的值,在之后询问和修改时再更新。
在更新后需要将标记下传
pushdown函数:
void pushdown(long long i){
if (tr[i].lazy != 0){
tr[i * 2].lazy += tr[i].lazy;
tr[i * 2 + 1].lazy += tr[i].lazy;
tr[i * 2].sum += tr[i].lazy * (tr[i * 2].r - tr[i * 2].l + 1);
tr[i * 2 + 1].sum += tr[i].lazy * (tr[i * 2 + 1].r - tr[i * 2 + 1].l + 1);
tr[i].lazy = 0;
}
return ;
}
新的区间修改和区间查询
void update(long long i,long long l,long long r,long long k){
if (tr[i].l > r || tr[i].r < l) return ;
if (tr[i].r <= r && tr[i].l >= l){
tr[i].sum += k * (tr[i].r - tr[i].l + 1);
tr[i].lazy += k;
return ;
}
pushdown(i);
long long mid = (tr[i].l + tr[i].r) >> 1;
if (l <= mid) update(i * 2,l,r,k);
if (r > mid) update(i * 2 + 1,l,r,k);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum;
}
long long query(long long i,long long l,long long r){
if (tr[i].l > r || tr[i].r < l) return 0;
if (tr[i].r <= r && tr[i].l >= l) return tr[i].sum;
pushdown(i);
long long mid = (tr[i].l + tr[i].r) >> 1;
long long ans = 0;
if (l <= mid) ans += query(i * 2,l,r);
if (r > mid) ans += query(i * 2 + 1,l,r);
return ans;
}
总代码(洛谷P3372 【模板】线段树 1 ):
#include <bits/stdc++.h>
using namespace std;
struct Tree {
long long l,r;
long long sum,lazy;
}tr[400050];
long long a[100050],n;
void build(long long i,long long l,long long r){
tr[i].l = l;
tr[i].r = r;
if (l == r){
tr[i].sum = a[l];
return ;
}
long long mid = (l + r) >> 1;
build(i * 2,l,mid);
build(i * 2 + 1,mid + 1,r);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum;
}
void pushdown(long long i){
if (tr[i].lazy != 0){
tr[i * 2].lazy += tr[i].lazy;
tr[i * 2 + 1].lazy += tr[i].lazy;
tr[i * 2].sum += tr[i].lazy * (tr[i * 2].r - tr[i * 2].l + 1);
tr[i * 2 + 1].sum += tr[i].lazy * (tr[i * 2 + 1].r - tr[i * 2 + 1].l + 1);
tr[i].lazy = 0;
}
return ;
}
void update(long long i,long long l,long long r,long long k){
if (tr[i].l > r || tr[i].r < l) return ;
if (tr[i].r <= r && tr[i].l >= l){
tr[i].sum += k * (tr[i].r - tr[i].l + 1);
tr[i].lazy += k;
return ;
}
pushdown(i);
long long mid = (tr[i].l + tr[i].r) >> 1;
if (l <= mid) update(i * 2,l,r,k);
if (r > mid) update(i * 2 + 1,l,r,k);
tr[i].sum = tr[i * 2].sum + tr[i * 2 + 1].sum;
}
long long query(long long i,long long l,long long r){
if (tr[i].l > r || tr[i].r < l) return 0;
if (tr[i].r <= r && tr[i].l >= l) return tr[i].sum;
pushdown(i);
long long mid = (tr[i].l + tr[i].r) >> 1;
long long ans = 0;
if (l <= mid) ans += query(i * 2,l,r);
if (r > mid) ans += query(i * 2 + 1,l,r);
return ans;
}
int main(){
ios :: sync_with_stdio(false);
int m;
cin >> n >> m;
for (int i = 1;i <= n;i++) cin >> a[i];
build(1,1,n);
for (int i = 1;i <= m;i++){
int op,l,r;
cin >> op >> l >> r;
if (op == 1){
int k;
cin >> k;
update(1,l,r,k);
}
else cout << query(1,l,r) << endl;
}
return 0;
}
五、细节
在使用线段树时,必须将数组开到数据范围的
4
n
4n
4n,因为在访问过程中,除了会访问有意义的
2
n
2n
2n个节点,还会再检查是否是叶子节点时再次向下访问,导致可能会访问的节点个数的大致是题目给出的数据范围的
4
4
4倍
线段树还有一个问题,它的常数比较大,所以一般在
4
∗
1
0
5
4*10^5
4∗105的范围下能够1秒运行完