初识线段树

线段树(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主的视频结合起来看,可以更好的理解线段树。

视频链接如下:线段树入门【力扣双周赛 79】LeetCode_哔哩哔哩_bilibili

C02【模板】线段树+懒标记 Luogu P3372 线段树——信息学竞赛算法_哔哩哔哩_bilibili

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值