1、线段树
线段树也被称为区间树,英文名为Segment Tree
或者Interval tree
,是一种高级的数据结构。它主要用于维护区间信息(要求满足结合律)。与树状数组相比,它可实现效率为O(logn) 的区间修改,还可以同时支持多种操作(加、乘),更具通用性。
2、线段树常用操作
(1)add操作,即实现数组arr([l,r])的某区间内([L, R]) 所有的数+C:
add(L, R, C, l, r, rt)
(2)updata操作,即实现数组arr([l,r])的某区间内([L, R]) 所有的数更新为V:
updata(L, R, V, l, r, rt)
(3)querry操作,即实现数组arr([l,r])的某区间内([L, R])的和:
querry(L,R,l, r, rt)
3、线段树的建立
(1)为实现通过数组建立一个满二叉树的形式,选择4N(N,size(arr))的大小构建,多出的位置补零。若数组的下标从1开始,节点为 i 的左孩子为 2*i ,右孩子为 2*i+1,则可利用位运算进行优化,i<<1 和 i<<1 | 1。
(2)懒惰标记,标记的含义:本区间已经被更新过了,但是子区间不会被更新(所谓的懒住了),被更新的信息是什么(区间求和只用记录有没有被访问过,而区间加减乘除等多种操作的问题则要记录进行的是哪一种操作)。
(3)相对标记和绝对标记
1)相对标记指的是可以共存的标记,且打标记的顺序与答案无关,即标记可以叠加。 比如说给一段区间中的所有数字都 + a ,我们就可以把标记叠加一下,比如上一次打了一个 + 1 的标记,这一次要给这一段区间 + 5 ,那么就把 + 1 的标记变成 + 6 。例如接下来add操作中的lazy标记。
2)绝对标记是指不可以共存的标记,每一次都要先把标记下传,再给当前节点打上新的标记。这些标记不能改变次序,否则会出错。 比如说给一段区间的数字重新赋值,或是给一段区间进行多种操作。例如update操作中的change标记和updata标记 。
4、add操作的具体实现流程(通过举例演示)
线段树:arr = [0,0,0,0,0,0,0,0], sum = [0,0,0,0,0,0,0,0]
Add(L, R, C, l, r, rt] :l = 1, r= 8; rt = 1;
(1)任务范围为:[L,R] = [1~4] , C= 2
(2)任务范围为:[L,R] = [5~8] , C= 2
(3)任务范围为:[L,R] = [1~8] , C= 2
(4)任务范围为:[L,R] = [1~4] , C= 2
分发策略:将父范围的懒更新(lazy[rt]),分发给子范围;左右子范围更新lazy和sum;最后将父范围的lazy置零。
5、updata操作的具体实现流程
updata(L, R, V, l, r, rt] :l = 1, r= 8; rt = 1;
(1)在add(3)的基础上进行更新操作,任务范围:[L, R] = [1-8] V = 2;
(2)任务范围:[L, R] = [1-4] V = 2;
6、代码实现
class SegmentTree{
//arr[]为原序列的信息从0开始,但在arr里是从1开始的
//sum[]模拟线段树维护区间和
//lazy[]为累加懒标记
//change[]为更新的值
//update[]为更新懒标记
public:
int MAXN;
int* arr;
int* sum;
int* lazy;
int* change;
int* update;
public:
SegmentTree(vector<int> origin){
MAXN = origin.size() + 1;
arr = new int[MAXN];
for(int i = 1; i < MAXN; i++){
arr[i] = origin[i-1];
}
sum = new int[MAXN << 2];
lazy = new int[MAXN << 2];
change = new int[MAXN << 2];
update = new int[MAXN << 2];
}
void build(int l, int r, int rt){
//在arr[l~r]范围上,去build, 1~N
//rt:这个范围在sum中的下标
if(l == r){
sum[rt] = arr[l];
return;
}
int mid = (l + r) >> 1;
//建造左子树
build(l, mid, rt<<1);
//建造右子树
build(mid+1, r, rt<<1 | 1);
//上传合并
pushUp(rt);
}
void pushUp(int rt){
sum[rt] = sum[rt<<1] + sum[rt<<1 | 1];
}
void add_func(int L, int R, int C, int l, int r, int rt){
//[L~R]任务范围
//[l~r]表达的范围
//rt 去哪找l, r范围上的信息
//任务的范围彻底覆盖了当前表达的范围
if(L <= l && R >= r){
sum[rt] += C * (r - l + 1);
lazy[rt] += C;
return;
}
//任务并没有把l..r全包住
//要把当前任务下发,检查是否下发
int mid = (l+r) >> 1;
//下发之前的lazy add任务
pushdown(rt, mid-l+1, r-mid);
//判断左孩子是否需要接到任务
if(L <= mid){
add_func(L, R, C, l, mid, rt<<1);
}
//判断右孩子是否需要接到任务
if(R > mid){
add_func(L, R, C, mid+1, r, rt<<1 | 1);
}
//左右孩子做完任务后,更新sum信息
pushUp(rt);
}
void update_func(int L, int R, int V, int l, int r, int rt){
if(L <= l && R >= r){
update[rt] = true;
change[rt] = V;
sum[rt] = (r - l + 1)*V;
lazy[rt] = 0;
return;
}
int mid = (r + l) >> 1;
pushdown(rt, mid-l+1, r-mid);
if(L <= mid){
update_func(L, R, V, l, mid, rt<< 1);
}
if(R > mid){
update_func(L, R, V, mid+1, r, rt<<1 | 1);
}
pushUp(rt);
}
long querry(int L, int R, int l, int r, int rt){
if(L <= l && R >= r){
return sum[rt];
}
int mid = (r + l) >> 1;
pushdown(rt, mid-l+1, r-mid);
long ans = 0;
if(L <= mid){
ans += querry(L, R, l, mid, rt<<1);
}
if(R > mid){
ans += querry(L, R, mid+1, r, rt<<1 | 1);
}
return ans;
}
void pushdown(int rt, int ln, int rn){
if(update[rt]){
update[rt << 1] = true;
update[rt<<1 | 1] = true;
change[rt << 1] = change[rt];
change[rt<<1 | 1] = change[rt];
sum[rt<<1] = change[rt]*ln;
sum[rt<<1 | 1] = change[rt]*rn;
lazy[rt<<1] = 0;
lazy[rt<<1 | 1] = 0;
update[rt] = false;
}
if(lazy[rt] != 0){
lazy[rt << 1] += lazy[rt];
sum[rt << 1] += lazy[rt] * ln;
lazy[rt << 1 | 1] += lazy[rt];
sum[rt<<1 | 1] += lazy[rt]*rn;
lazy[rt] = 0;
}
}
void PrintArr(int* arr){
for(int i = 0; i < MAXN; i++){
cout << arr[i] << ",";
}
}
};