1. 线段树
1.1. 功能
对线性表做快速的区间修改、区间查询
1.2. 原理:分治
将每个区间平分为左右两个处理,建二叉树
1.3. 操作
1.3.1. 建树
从下往上合并
把树上所有点都便利一遍,时间复杂度
O
(
n
)
O(n)
O(n)
合并操作merge/pushUp:
求区间和、最大、最小值等可由子区间数值计算父区间数值的,才能使用线段树维护
区间和:
v
父
=
v
左
+
v
右
最小值:
v
父
=
m
i
n
(
v
左
,
v
右
)
最大值:
v
父
=
m
a
x
(
v
左
,
v
右
)
字符串哈希:
v
父
=
v
左
∗
P
l
e
n
(
右
)
+
v
右
(
P
为进制,不是自然溢出法的要取模)
.
.
.
.
.
.
区间和:v_父=v_左+v_右\\ 最小值:v_父=min(v_左,v_右)\\ 最大值:v_父=max(v_左,v_右)\\ 字符串哈希:v_父=v_左*P^{len(右)}+v_右\\ (P为进制,不是自然溢出法的要取模)\\ ......
区间和:v父=v左+v右最小值:v父=min(v左,v右)最大值:v父=max(v左,v右)字符串哈希:v父=v左∗Plen(右)+v右(P为进制,不是自然溢出法的要取模)......
后文将要维护的数值统称为
v
v
v
1.3.2. 单点查询
查i
,递归,
看i
在该区间的左子区间还是右子区间,
进入子区间接着查,查到叶子节点直接返回
例如:查找
[
8
,
8
]
[8,8]
[8,8]
时间复杂度 O ( l o g 2 n ) O(log_2n) O(log2n)
1.3.3. 单点修改
同上,查到叶子节点,再一层一层往回改父区间(merge)
时间复杂度
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
1.3.4. 区间查询
查
[
l
,
r
]
[l,r]
[l,r],递归,
#1、若覆盖该区间,直接返回
#2、若在左子区间,递归左子
#3、若在右子区间,递归右子
#4、若左右都有,递归两头
时间复杂度
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
分析(以上图为例):
情况#1,#2,#3不用说,
情况#4的时候左右对称同理,以左为例:
若覆盖要查询的区间覆盖左孩子的右孩子,则在左孩子的右孩子处直接返回
如,查
[
2
,
5
]
[2,5]
[2,5],
则在内侧
[
4
,
5
]
[4,5]
[4,5]直接返回,在
[
1
,
3
]
[1,3]
[1,3]继续搜;
若覆盖要查询的区间不覆盖左孩子的右孩子,则一定不覆盖左孩子的左孩子
如,查
[
5
,
5
]
[5,5]
[5,5],
则只往内侧
[
4
,
5
]
[4,5]
[4,5]走,不查
[
1
,
3
]
[1,3]
[1,3]
故要么走左边,右边走一步就停(一串紫的挂着一串红的,log)
要么只走右边,(一串紫的,log)
要么是以上两种的组合(log)
所以复杂度
O
(
l
o
g
2
n
)
O(log_2n)
O(log2n)
1.3.5. 区间修改
1.3.5.1. 错误做法
查询要改的区间,然后直接改
错因:
只改了父区间,没改子区间,查询子区间会出错
如:
整个序列值都为
1
1
1,线段树维护区间最大值
先把
[
1
,
10
]
[1,10]
[1,10]中每个元素加
1
1
1
再查询 [ 1 , 1 ] [1,1] [1,1]
则错误方法返回 1 1 1(应该为 1 + 1 = 2 1+1=2 1+1=2)
1.3.5.2. 懒标记(lazy)
1.3.5.2.1. 定义:
该节点的lazy表示该节点自己的
v
v
v已修改,该节点的所有子节点
v
v
v未修改
(根据需要,每个节点可以有多个lazy,如等差数列的首项和公差)
1.3.5.2.2. 下放懒标记(pushDown):
1、子区间根据父区间
l
a
z
y
lazy
lazy修改
v
v
v
2、子区间根据父区间
l
a
z
y
lazy
lazy修改
l
a
z
y
lazy
lazy
3、父区间
l
a
z
y
lazy
lazy清空
1.3.5.2.3. 作用:
用了lazy,先查要改的区间,遇到#1情况,则将此区间的 v v v和 l a z y lazy lazy都修改,则此区间的数据已经正确,只需必要时将lazy下放给子区间即可
1.3.5.2.4. 下放时机:
1、区间查询中,情况#2,#3,#4需要用到子区间的
v
v
v,故需要pushDown
2、区间修改需要pushDown,否则merge操作时左、右孩子的
v
v
v均未修改,父区间的
v
v
v会错误
如:
整个序列值都为
1
1
1,线段树维护区间最大值,
l
a
z
y
lazy
lazy表示增加量
1、把 [ 1 , 10 ] [1,10] [1,10]中每个元素加 1 1 1
2、查询 [ 6 , 10 ] [6,10] [6,10]
返回2,正确
3、把
[
4
,
5
]
[4,5]
[4,5]中每个元素加
1
1
1
(1)正确示范:
在
[
1
,
5
]
[1,5]
[1,5]处pushDown
修改 [ 4 , 5 ] [4,5] [4,5]的 v v v和 l a z y lazy lazy
一路往上merge
(2)错误示范:区间修改没有pushDown
修改
[
4
,
5
]
[4,5]
[4,5]的
v
v
v和
l
a
z
y
lazy
lazy
merge
发现
v
v
v都改错了
因此pushDown操作在区间修改中也一定要做!!
1.4. 实现
维护长度为n
的序列
n
∈
(
0
,
N
)
n \in (0,N)
n∈(0,N),初始值为a[i]
,
i
∈
[
0
,
n
)
i \in [0,n)
i∈[0,n)
struct Node{
int l;int r;
int v;int lazy;
};
Node tr[N*4];
/*
用数组实现线段树,记得开4*N
叶子结点一共:N
父亲节点一共:N-1
叶子结点下一层:2*N (防止越界)
故总共4*N
另外,我把tr[0]当根
用tr[1]当根也可,但算孩子下标会不一样
*/
void build(int x,int l,int r){
if(l==r){//到叶子了
tr[x]=Node{l,r,a[l],0};
return;
}
int xl=x*2+1;int xr=xl+1;
int mid=(l+r)/2;
build(xl,l,mid);
build(xr,mid+1,r);
//merge
tr[x]=Node{
l,r,
max(tr[xl].v,tr[xr].v),0
};
}
inline void pushDown(int x){
if(tr[x].lazy){
int xl=x*2+1;int xr=xl+1;
tr[xl].lazy+=tr[x].lazy;
tr[xl].v+=tr[x].lazy;
tr[xr].lazy+=tr[x].lazy;
tr[xr].v+=tr[x].lazy;
tr[x].lazy=0;
}
}
//区间查询
int query(int x,int l,int r){
int mid=(tr[x].l+tr[x].r)/2;
int xl=x*2+1;int xr=xl+1;
if(l<=tr[x].l&&tr[x].r<=r){//#1
return tr[x].v;
}
pushDown(x);
if(r<=mid){//#2
return query(xl,l,r);
}
else if(mid+1<=l){//#3
return query(xr,l,r);
}
else{//#4
return max(
query(xl,l,mid),
query(xr,mid+1,r)
);
}
}
//区间修改
void add(int x,int l,int r,int y){
int mid=(tr[x].l+tr[x].r)/2;
int xl=x*2+1;int xr=xl+1;
if(l<=tr[x].l&&tr[x].r<=r){//#1
tr[x].lazy+=y;
tr[x].v+=y;
return;
}
pushDown(x);
if(r<=mid){//#2
add(xl,l,r,y);
}
else if(mid+1<=l){//#3
add(xr,l,r,y);
}
else{//#4
add(xl,l,mid,y);
add(xr,mid+1,r,y);
}
//merge
tr[x].v=max(tr[xl].v,tr[xr].v);
}