[高级数据结构]线段树SegmentTree
知道你没耐心看完,先一边听歌一边把模板抄一遍,你会懂的
刚学了线段树
下面对灵神的模板解释一下
结构讲解
线段树是一个完全二叉树,我们用数组模拟存储
node是什么
整棵树的根节点是1
- 当前根节点编号为
node
- 左子树的根节点为
node<<1
(node*2) - 右子树的根节点为
node<<1|1
(node*2+1)
node如何对应原数组区间
node=1
表示整棵树的根,它管理的区间是[0, n-1]
node=2
是左子树,它管理 根区间 的左半部分[0, mid]
node=3
是右子树,它管理 根区间 的右半部分[mid+1, n-1]
依次递归,每个node
代表一个子区间
- 当前根节点
node
,管理区间[l,r]
,中点int m=l+r>>1;
- 左子树根节点
node<<1
,管理区间[l,m]
- 右子树根节点
node<<1|1
,管理区间[m+1,r]
注意
每个node
对应的区间[l,r]
是固定的 但是我们在写函数的时候要用到很多递归,自己预处理一个node与区间的映射太麻烦了,所以就把l,r
当做参数传递了
最朴素的模板
晕递归的同学,直接把递归调用看做他宏观整体上实现了这个功能,不要细究里面具体怎么实现的
#include<bits/stdc++.h>
using namespace std;
template<typename T>//T会自动推导变量类型,T的作用域只在这个class内 其他地方还可以用T当做变量名
class SegmentTree{
private:
int n;
vector<T>tree;
T merge_val(T a,T b) {//合并两个值
return max(a,b);//根据题目修改,可能是区间和或者最小值
}
void maintain(int node){//根据左右子节点的值更新当前作为父结点的node的值
tree[node]=merge_val(tree[node<<1],tree[node<<1|1]);
}
void build(vector<T>&a,int node,int l,int r){
if(l==r){//找到叶子节点,可以直接赋值了
tree[node]=a[l];
return;
}
int m=l+r>>1;
build(a,node<<1,l,m);
build(a,node<<1|1,m+1,r);
maintain(node);//递归建完左右子树后,要更新当前根节点的值
}
void update(int node,int l,int r,int i,T val){
if(l==r){//找到叶子节点 直接更新
//这里也根据题目修改 想直接替换就直接赋值为val
tree[node]=merge_val(tree[node],val);
return;
}
int m=l+r>>1;
if(i<=m){
update(node<<1,l,m,i,val);
}else{
update(node<<1|1,m+1,r,i,val);
}
maintain(node);//递归更新左右子树后,要更新当前根节点的值
}
T query(int node,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr){
//当前子树完全被查询范围包含,[l,r]是[ql,qr]的子集,直接返回
return tree[node];
}
int m=l+r>>1;
if(qr<=m){//查询区间[ql,qr]在node的左子树
return query(node<<1,l,m,ql,qr);
}
if(ql>m){//查询区间[ql,qr]在node的右子树
return query(node<<1|1,m+1,r,ql,qr);
}
//如果到这里还没return,说明ql<=m<=qr
//ql,qr在m的两边
T l_res=query(node<<1,l,m,ql,qr);
T r_res=query(node<<1|1,m+1,r,ql,qr);
return merge_val(l_res,r_res);
}
public:
//写了两个构造函数
SegmentTree(int n,T init_val): SegmentTree(vector<T>(n,init_val)){}
SegmentTree(vector<T>&a): n(a.size()),tree(4*a.size()){//开四倍空间
build(a,1,0,n-1);//数组,根节点,对应的区间(就是整个数组)
}
void update(int i,T val){
update(1,0,n-1,i,val);//根节点,对应的区间(就是整个数组),i为原数组的下标,需要更新的值
}
T query(int ql,int qr){
return query(1,0,n-1,ql,qr);//根节点,对应的区间(就是整个数组),查询区间
}
T get(int i){
return query(1,0,n-1,i,i);//根节点,对应的区间(就是整个数组),单点查询
}
};
int n;
vector<int>a(n);
SegmentTree t(a);
常见应用
min,max,gcd,lcm,sum,xor
- 维护区间内符合某种条件的 元素个数
懒标记LazyTag
- 懒标记就是用来延迟更新数组每个点具体的值
在访问子树之前要把懒标记分配下去 - 访问左右子树之前需要spread
- 子树的根上的 todo(懒标记) 代表整个子树里所有点需要增加的量,但这个增量还没有真正作用到子树的 val 上,只有当 spread 被调用时,才会把这个 todo 传播到左右子树,并更新它们的 val 和 todo
- 当子树变化时 必须马上maintain更新根节点
模板
#include<bits/stdc++.h>
using namespace std;
template<typename T, typename F>
class LazySegmentTree{//求区间和
private:
const F TODO_INIT=0;//懒标记初始值 根据题目修改
struct Node{
T val;
F todo;
};
int n;
vector<Node>tree;
T merge_val(T a,T b){
return a+b;//求区间和 两个值直接相加
}
F merge_todo(F a,F b){
return a+b;//合并懒标记 因为求区间和 懒标记直接加在点上 所以懒标记可以直接相加合并
}
//把懒标记作用在node子树 更新val ”在当前子树做任务“
void apply(int node,int l,int r,F todo){
Node& cur=tree[node];
//先把这个子树的总体val更新了 因为很容易得到
cur.val+=todo*(r-l+1);//区间长度*每个点增量
//具体数组每个点的值用懒标记叠加上 下一行就是在合并懒标记
cur.todo=merge_todo(todo,cur.todo);
}
//把当前节点的懒标记传递给左右子树 “给下属分派任务”
void spread(int node,int l,int r){
Node& cur=tree[node];
F todo=cur.todo;
if(todo==TODO_INIT){//没有懒标记 就return
return;
}
//根节点的懒标记 就是下面的子节点需要更新的增量
int m=l+r>>1;
apply(node<<1,l,m,todo);//分治递归更新
apply(node<<1|1,m+1,r,todo);
cur.todo=TODO_INIT;//下传完毕 重置根节点的懒标记
}
void maintain(int node){//维护根节点的值
tree[node].val=merge_val(tree[node<<1].val,tree[node<<1|1].val);
}
void build(vector<T>&a,int node,int l,int r){
Node& cur=tree[node];
cur.todo=TODO_INIT;//记得初始化懒标记
if(l==r){
cur.val=a[l];
return;
}
int m=l+r>>1;
build(a,node<<1,l,m);
build(a,node<<1|1,m+1,r);
maintain(node);
}
void update(int node,int l,int r,int ql,int qr,F f){
if(ql<=l&&r<=qr){
apply(node,l,r,f);
return;
}
//先把任务分派下去 才能更新下面的值
spread(node,l,r);
int m=l+r>>1;
//这里是跟朴素版本不一样的
if(ql<=m){//目标区间有一部分在m左边 递归更新左子树
update(node<<1,l,m,ql,qr,f);
}
if(qr>m){//目标区间有一部分在m右边 递归更新右子树
update(node<<1|1,m+1,r,ql,qr,f);
}
maintain(node);
}
T query(int node,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr){
return tree[node].val;
}
//访问子树之前要spread
spread(node,l,r);
int m=l+r>>1;
if(qr<=m){//目标区间在左子树
return query(node<<1,l,m,ql,qr);
}
if(ql>m){//目标区间在右子树
return query(node<<1|1,m+1,r,ql,qr);
}
//目标区间跨m的左右两边
T l_res=query(node<<1,l,m,ql,qr);
T r_res=query(node<<1|1,m+1,r,ql,qr);
return merge_val(l_res,r_res);
}
public:
LazySegmentTree(int n,T init_val=0) :LazySegmentTree(vector<T>(n,init_val)){}
LazySegmentTree(vector<T>&a) : n(a.size()),tree(4*a.size()){
build(a,1,0,n-1);
}
//用f更新[ql,qr]中的每个a[i]
void update(int ql,int qr,F f){
update(1,0,n-1,ql,qr,f);
}
T query(int ql,int qr){
return query(1,0,n-1,ql,qr);
}
};
int n;
vector<int>a(n);
LazySegmentTree<int,int> t(a);//要自己写明类型