线段树(Segment Tree)是一种可以在数组上进行高效区间查询和区间更新的数据结构。它广泛应用于处理区间问题,比如区间求和、区间最小值/最大值查询以及区间更新等。线段树本质上还是采用了分治的思想。
1. 线段树的基本结构
线段树是一棵完全二叉树,每个节点表示一个区间(通常是一个数组的某个子区间),叶子节点表示数组中的每个元素,内部节点表示它们对应区间的某种聚合值(如区间和、区间GCD、最小值、最大值等)。
2. 基本操作
2.1 区间查询
对于一个区间查询操作(比如求区间和、区间最大值、区间最小值等),我们可以通过递归遍历线段树,将整个区间分解为几个小的子区间,最终得到答案。
2.2 区间更新
线段树支持区间更新(比如区间加法、区间赋值等),通过懒惰标记(lazy propagation)来优化。
3. 代码模板
#include <bits/stdc++.h>
#define long long int
#define lc p<<1
#define rc (p<<1) | 1
#define N 500005
using namespace std;
struct node{
int l,r,sum,add;
}tr[N*4];
int m,n;
int w[N];
void pushup(int p){
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void pushdown(int p){
if(tr[p].add){
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[lc].add += tr[p].add;
tr[rc].add += tr[p].add;
tr[p].add = 0;
}
}
void build(int p,int l,int r){
tr[p] = {l,r,w[l],0};
if(l == r) return;
int m = (l+r)>>1;
//递归构建左右子树
build(lc,l,m);
build(rc,m+1,r);
pushup(p);
}
void update(int p,int x,int y,int k){
if(x <= tr[p].l && tr[p].r <= y){
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return;
}
int m = (tr[p].l + tr[p].r) >> 1;
if(x <= m) update(lc,x,y,k);
if(y > m) update(rc,x,y,k);
pushup(p);
}
int query(int p,int x,int y){
//完全覆盖直接返回
if(x <= tr[p].l && tr[p].r <= y){
return tr[p].sum;
}
int sum = 0;
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
//有重叠部分 就分裂
if(x <= m) sum += query(lc,x,y);
if(y > m) sum += query(rc,x,y);
return sum;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cin>>m>>n;
for(int i = 1;i<=m;i++){
cin>>w[i];
}
build(1,1,m);
//根据题意进行区间查询或者区间修改操作
return 0;
}
4. 代码解释
4.1 宏定义
#define long long int
#define lc p<<1
#define rc (p<<1) | 1
#define N 500005
用int表示long long 防止在计算区间和的时候爆int,lc、rc分别表示根节点编号为p时的左右子树编号,左子树为2*p,即p<<1;右子树为2*p+1,即(p<<1) | 1,N代表数组的容量。
4.2 结构体
struct node{
int l,r,sum,add;
}tr[N*4];
每个结构体可以想象成树的节点,节点信息包括l(区间左端点)、r(区间右端点)、sum(区间和)、add(lazy标记),注意区间采用的是闭区间。
4.3 函数
void pushup(int p){
tr[p].sum = tr[lc].sum + tr[rc].sum;
}
void pushdown(int p){
if(tr[p].add){
tr[lc].sum += tr[p].add * (tr[lc].r - tr[lc].l + 1);
tr[rc].sum += tr[p].add * (tr[rc].r - tr[rc].l + 1);
tr[lc].add += tr[p].add;
tr[rc].add += tr[p].add;
tr[p].add = 0;
}
}
pushup函数:用于自底向上计算根节点的区间和,根节点的区间和 = 左儿子节点的区间和 + 右儿子节点的区间和。
pushdown函数:该函数主要作用是下传懒标记,如果当前节点的懒标记不为0,那么就更新孩子的区间和以及将懒标记传递给左右儿子。可以将懒标记想象成一个账本,上边记录着父亲欠孩子们多少钱,还给孩子们钱后将自己的账本清零。孩子们的区间和应该增加多少呢?应该是孩子的区间长度 * 懒标记的值。
void build(int p,int l,int r){
tr[p] = {l,r,w[l],0};
if(l == r) return;
int m = (l+r)>>1;
//递归构建左右子树
build(lc,l,m);
build(rc,m+1,r);
pushup(p);
}
build函数:该函数是用来构建线段树的,调用时需要传入根节点编号(一般是1),还有数组区间的范围, tr[p] = {l,r,w[l],0};一开始将初始化w[l]是没有作用的,当遍历到的节点是叶子节点(l == r)时,该节点的区间和就是w[l] 或 w[r],通过递归,可以构建左右子树,在回溯返回的时候,要用左右孩子的区间和更新父节点的区间和,这时候就用到了pushup函数。
void update(int p,int x,int y,int k){
if(x <= tr[p].l && tr[p].r <= y){
tr[p].sum += k * (tr[p].r - tr[p].l + 1);
tr[p].add += k;
return;
}
int m = (tr[p].l + tr[p].r) >> 1;
if(x <= m) update(lc,x,y,k);
if(y > m) update(rc,x,y,k);
pushup(p);
}
update函数:该函数是用来更新区间的值的,常用于区间修改。如果当前节点完全被要查询区间覆盖,那么直接修改该区间的和,并打上懒标记,不在更新它的孩子,当再次用到他们的使用会通过下传懒标记(pushdown)更新孩子的区间和。如果当前节点的区间没有被完全覆盖,那么就分裂节点,看左右孩子哪个区间和要查询的区间有交集,通过递归更新。最后别忘了更新父节点的区间和
int query(int p,int x,int y){
//完全覆盖直接返回
if(x <= tr[p].l && tr[p].r <= y){
return tr[p].sum;
}
int sum = 0;
pushdown(p);
int m = (tr[p].l + tr[p].r) >> 1;
//有重叠部分 就分裂
if(x <= m) sum += query(lc,x,y);
if(y > m) sum += query(rc,x,y);
return sum;
}
query函数:区间查询是通过分解区间,然后计算每个区间的和来实现的,如果当前节点的区间完全被要查询的区间覆盖,那么该区间就是一个子区间,直接返回区间和。在向下分解区间前,一定要先pushdown,将父亲欠的账还完,然后通过判断是否有区间重叠,计算区间和。
5.建议
可以将b站的两位up主的视频结合起来看,可以更好的理解线段树。