算法竞赛进阶指南0x43 线段树

线段树是算法竞赛中常用的数据结构,用于高效维护区间信息。本文通过P3372模板题介绍线段树的建立和区间修改,强调了懒标记在区间修改中的关键作用,帮助理解线段树如何处理区间查询和更新操作。
摘要由CSDN通过智能技术生成

        线段树(Segment Tree)几乎是算法竞赛最常用的数据结构了,它主要用于维护区间信息(要求满足结合律)。与树状数组相比,它可以实现 O(log⁡n) 的区间修改,还可以同时支持多种操作(加、乘),更具通用性。

        接下来我们用这道模板题为例,看看线段树是怎么维护区间和这一信息的。 

P3372 【模板】线段树 1

线段树的建立

        线段树是一棵平衡二叉树。母结点代表整个区间的和,越往下区间越小。注意,线段树的每个节点都对应一条线段(区间),但并不保证所有的线段(区间)都是线段树的节点,这两者应当区分开。

如果有一个数组[1,2,3,4,5],那么它对应的线段树大概长这个样子:

       $$ 每个节点x的左右子节点分别是 2 * x 和 2 * x + 1 $$ 

$$ 不论 x 是奇数还是偶数,2 * x都是偶数,那么 2 * x + 1就等于 (2 * x) | 1 $$

$$ 乘以二相当于左移一位,即 << 1 $$

$$ 而左移运算 << 的优先级高于按位或运算符 | $$

$$ 因此对于先执行左移运算后执行按位或运算的情况,我们不需要加括号改变优先级 $$

$$ 因此 x 的子节点可以表示为 x << 1 和 x << 1 | 1 $$

#define ls x << 1
#define rs x << 1 | 1
//ls:表示左子节点,rs:表示右子节点(leftson,rightson)

如何从数组建立一棵线段树?我们可以考虑递归地进行。

void build(int x, int l, int r) {
    if (l == r) {
        //到达叶子节点
        return void(tree[x] = A[l]);
        //用数组中的数据赋值
    }
    int mid = l + r >> 1;
    build(ls, l, mid);
    build(rs, mid + 1, r);
    //先建立左右子节点
    tree[x] = tree[ls] + tree[rs];
    //该节点的值等于左右子节点的值之和
}

这里用一张gif展现上述的过程:

区间修改

        在讲区间修改前,要先引入一个“懒标记”(或延迟标记)的概念。懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记,将来要用到它的子区间的时候,再向下传递

void update(int l, int r, int value, int x = 1, int cl = 1, int cr = n) {
    // 给A[l] ~ A[r] 都加上 value
    if (cl > r || cr < l) {
        //区间无交集
        return;
        //剪枝
    }
    else if (cl >= l && cr <= r) {
        //当前节点对应的区间包含在目标区间中
        tree[x] += value * (cr - cl + 1);
        //更新当前区间的值
        if (cr > cl) {
            //不是叶子节点
            mark[x] += value;
            //给当前区间打上标记
        }
    } else {
        //与目标区间有交集,但不包含于其中
        int mid = l + ((r - l) >> 1);
        mark[ls] += mark[x];
        //标记向下传递
        mark[rs] += mark[x];
        tree[ls] += mark[x] * (mid - cl + 1);
        //往下更新一层
        tree[rs] += mark[x] * (cr - mid);
        mark[x] = 0;//消除标记
        update(l, r, value, ls, cl, mid);
        //递归地往下寻找
        update(l, r, value, rs, mid + 1, cr);
        tree[x] = tree[ls] + tree[rs];
        //根据子节点更新当前节点的值
    }
}

        更新时,我们是从最大的区间开始,递归向下处理。注意到,任何区间都是线段树上某些节点的并集。于是我们记目标区间为 [L,R] ,当前区间为 [CL,CR] , 当前节点为 X ,我们会遇到三种情况: 

1. 当前区间与目标区间没有交集:

这时直接结束递归。

2.当前区间被包括在目标区间里:

这时可以更新当前区间,别忘了乘上区间长度:

tree[x] += (cr - cl + 1) * value;

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值