带懒标记线段树(区间修改&区间查询)

线段树是基于分治思想的二叉树,用来维护区间信息(区间和,区间最值,区间GCD等),可以在log(n)的时间内执行区间修改和区间查询

线段树中每个叶子结点存储元素本身非叶子结点存储区间内元素的统计

(视频学习:【C02【模板】线段树+懒标记 Luogu P3372 线段树 1】https://www.bilibili.com/video/BV1G34y1L7b3?vd_source=4c9eb38d8205116069b961c84f64c958)

在这里插入图片描述

线段树结构

1、结点数组tr[]

结构体包含三个变量:l, r, sum
l,r存区间的左右端点,sum存区间和

2、递归建树

父节点编号为p

左孩子编号为 2 ∗ p 2*p 2p (p<<1),右孩子编号我 2 ∗ p + 1 2 * p + 1 2p+1 (p<<1|1)

3、代码实现

#include<iostream>
#define lc p<<1
#define rc p<<1|1
#define N 500005
using namespace std;
int n, w[N], ns;
struct node {
int l, r, sum;
}tr[N*4]; // N*4大小数组范围,如果想要了解,可以看上方链接视频

void build (int p, int l, int r) {
    ns++;
    tr[p] = {l, r, w[l]}; // 一开始对于前面的结点没有实质性影响,对于叶子节点刚好吻合,而在前面的结点可以通过叶子节点或后面结点递归回溯求出来
    if(l == r){
        cout << tr[p].l << ' ' << tr[p].r << ' ' << tr[p].sum << '\n';
        return; 
    } 
    int m = l + r >> 1;
    build(lc, l, m);
    build(rc, m + 1, r);
    tr[p].sum = tr[lc].sum + tr[rc].sum;
    cout << tr[p].l << ' ' << tr[p].r << ' ' << tr[p].sum << '\n';
}
int main() {
    int n; cin >> n;
    for(int i = 1; i <= n; i++) cin >> w[i];
    build(1, 1, n);
    return 0;
}


如上代码运行结果,可以看出与上面图中树结构中每个结点相同

线段树操作

一、点修改

回顾下树状数组中更新某个点的值,需要通过+lowbit操作来维护t数组,但是我们这里使用线性数组tr[]来实现线段树这种二叉树结构,其中每个结点编号与原数组下标并没有太大关系,原数组的下标信息被存储在了结点内(l, r, sum),因此对原数组修改我们需要从树的根节点进行二分依次修改。

void update(int p, int x, int k) { // 对结点p进行修改,将原数组x位置上加上k值
    if(tr[p].l == x && tr[p].r == x) {
        tr[p].sum += k;
        return;
    }
    int m = tr[p].l + tr[p].r >> 1;
    if(m >= x) update(lc, x, k); // 这里是≥是因为在建树过程中左子树建立是包含m的,右子树是从m+1开始,所以对lc的判定要加上=
    else update(rc, x, k);
    tr[p].sum = tr[lc].sum + tr[rc].sum;
}


在这里插入图片描述

二、区间查询

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
在这里插入图片描述

int queue(int p, int x, int y) { //查询[x, y]区间和,p表示当前查询结点, 函数有返回值
    cout << "入" << p << '\n';
    if(x <= tr[p].l && y >= tr[p].r) {
        cout << "回 " << p;
        cout << " re " << tr[p].sum << '\n'; 
        return tr[p].sum;
    }
    int m = tr[p].l + tr[p].r >> 1;
    int ans = 0;
    if(x <= m) ans += queue(lc, x, y);
    if(y > m) ans += queue(rc, x, y);
    cout << "回 " << p;
    cout << " ans " << ans << " re " << ans << '\n';
    return ans;
} 

在这里插入图片描述

三、区间修改

​ 对于区间修改,我们很容易想到差分,但是差分适合多次区间修改后查找某个位置上的值,并不适合区间修改后查询某个区间的问题。这里可以用线段树懒标记来实现。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
#define lc p<<1
#define rc p<<1|1
#define N 50010
struct node{
    int l, r, sum, add;
}tr[4*N];

int n, 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[lc].r - tr[lc].l + 1) * tr[p].add;
        tr[rc].sum += (tr[rc].r - tr[rc].l + 1) * tr[p].add;
        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 = tr[p].l + tr[p].r >> 1;
    build(lc, l, m);
    build(rc, m + 1, r);
    pushup(p);
}
void update(int p, int x, int y, int k) {
    cout << "入" << p << '\n';
    if(x <= tr[p].l && tr[p].r <= y) {
        tr[p].sum += (tr[p].r - tr[p].l + 1) * k;
        tr[p].add += k;
        cout << "回 " << p << " sum " << tr[p].sum << " add " << tr[p].add << '\n';
        return;
    }
    int m = tr[p].l + tr[p].r >> 1;
    pushdown(p);
    if(x <= m) update(lc, x, y, k);
    if(y > m) update(rc, x, y, k);
    pushup(p);
    cout << "回 " << p << " sum " << tr[p].sum << '\n';
}
int query(int p, int x, int y) {
    if(x <= tr[p].l && tr[p].r <= y) {
        return tr[p].sum;
    }
    int m = tr[p].l + tr[p].r >> 1;
    pushdown(p);
    int sum = 0;
    if(x <= m) sum += query(lc, x, y);
    if(y > m) sum += query(rc, x, y);
    return sum;
}
int main() {
    cin >> n; 
    for(int i = 1; i <= n; i++) cin >> w[i];
    build(1, 1, n);
    update(1, 4, 9, 5);
    cout << query(1, 4, 5);
    return 0;
}

例题:

P3372 【模板】线段树 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这是一个区间进行修改,区间查询的模板题,直接使用带有懒标记的线段树模板,这里有一些细节注释在代码中

#include<bits/stdc++.h>
using namespace std;
#define lc p<<1  // 这里不要写成右移,因为这个调了好一会
#define rc p<<1|1
const int N = 1e5 + 10;
using ll = long long;

struct node {
    int l, r;
    ll sum;
    int add;
}tr[4*N];

int n, m;
ll w[N];

void pushdown(int p) {
    if(tr[p].add) {
        tr[lc].sum += (tr[lc].r - tr[lc].l + 1) * tr[p].add;
        tr[rc].sum += (tr[rc].r - tr[rc].l + 1) * tr[p].add;
        tr[lc].add += tr[p].add;
        tr[rc].add += tr[p].add;
        tr[p].add = 0;
    }
}
void pushup(int p) {
    tr[p].sum = tr[lc].sum + tr[rc].sum;
} 

void build(int p, int l, int r) {
    tr[p] = {l, r, w[l], 0}; //带有懒标记,不要忘记0哦
    if(l == r) return; 
    int m = tr[p].l + tr[p].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 += (tr[p].r - tr[p].l + 1) * k;
        tr[p].add += k;
        return;
    }
    int m = tr[p].l + tr[p].r >> 1;
    pushdown(p);
    if(x <= m) update(lc, x, y, k);
    if(y > m) update(rc, x, y, k);
    pushup(p);
}
//题目注意要开long long
ll query(int p, int x, int y) {
    if(x <= tr[p].l && tr[p].r <= y) {
        return tr[p].sum;
    }    
    int m = tr[p].l + tr[p].r >> 1;
    pushdown(p); 
    ll ans = 0;
    if(x <= m) ans += query(lc, x, y);
    if(y > m) ans += query(rc, x, y);  //这需要查询,没有修改,就没必要用pushup函数
    return ans;
}
int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> w[i];
    build(1, 1, n);    
    for(int i = 1; i <= m; i++) {
        int op; cin >> op;
        if(op == 1) {
            int x, y, k; cin >> x >> y >> k;
            update(1, x, y, k);
        }
        else {
            int x, y; cin >> x >> y;
            cout << query(1, x, y) << '\n';
        }
    }
    return 0;
}

后序有题目会更新本文章

这里推荐如果是单点修改的区间查询,可以用树状数组实现,代码少不用建树。如果是区间修改&&区间查询的话,就需要用懒标记线段树。

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

幻听嵩的留香

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值