快速搞定算法之线段树问题

快速搞定算法之线段树问题

线段树主要是用于区间问题,比如求区间和、区间修改、区间最值等。

在这里插入图片描述

树状数组其实就是阉割版的线段树,但线段树能做到的事比树状数组要大。

这里讲解两道使用线段树解决的问题:

区间求和问题 :

题目描述

如题,已知一个数列,你需要进行下面两种操作:

将某区间每一个数加上 k。
求出某区间每一个数的和。
输入格式

第一行包含两个整数 n,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。

接下来 m 行每行包含 3 或 4 个整数,表示一个操作,具体如下:

1 x y k:将区间 [x,y] 内每个数加上 k。
2 x y:输出区间 [x,y] 内每个数的和。
输出格式

输出包含若干行整数,即为所有操作 2 的结果。

输入输出样例

  • 输入 #1

    5 5

    1 5 4 2 3

    2 2 4

    1 2 3 2

    2 3 4

    1 1 5 1

    2 1 4

  • 输出
    1

    11

    8

    20

说明/提示

对于 30% 的数据:n≤8,m≤10。
对于 70% 的数据:n≤103,m≤104。
对于 100% 的数据:1≤n,m≤105。

保证任意时刻数列中任意元素的和在 [263,263) 内。

【样例解释】

这道题是模板题,直接使用线段树方法,求区间和,则在线段树结点里面存放包含区间的和。

线段树的基本概念上节课已经讲了,需要建立一颗区间树,数结点里面存放我们需要的信息,然后维护这棵树。

思路先是需要建树,可以选择数组建树,区间划分就采用二分。线段树根节点为1,所以每一个编号为i的结点,其左儿子编号为i2,右儿子编号为i2+1。构建树可以使用递归的方法。当区间只有一个值时,这个结点的值就是这一个值。

建树方法:

void build(int s, int t, int p) {

// 对 [s,t] 区间建立线段树,当前根的编号为 p

if (s == t) {

d[p] = a[s];

return;

}

int m = (s + t) / 2;

build(s, m, p * 2), build(m + 1, t, p * 2 + 1);

// 递归对左右区间建树

d[p] = d[p * 2] + d[(p * 2) + 1];

}

线段树构建完了就是该怎么通过它进行区间查询了。

int getsum(int l, int r, int s, int t, int p) {

// [l,r] 为查询区间,[s,t] 为当前节点包含的区间,p 为当前节点的编号

if (l <= s && t <= r)

return d[p]; // 当前区间为询问区间的子集时直接返回当前区间的和

int m = (s + t) / 2, sum = 0;

if (l <= m) sum += getsum(l, r, s, m, p * 2);

// 如果左儿子代表的区间 [l,m] 与询问区间有交集,则递归查询左儿子

if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);

// 如果右儿子代表的区间 [m+1,r] 与询问区间有交集,则递归查询右儿子

return sum;

这里发现,建树和查询都没有看到更新的操作,当某一区间的值都加上一个值以后还需要对区间进行更新。因为当把区间中每一个值都遍历一次、修改一次,那耗费的时间会很多,而且就不能体现线段树的优势了。使用这里使用一个叫做 懒惰标记 的东西。

在进行区间增减时,可以把区间的增减值先放在结点懒惰标记里面,也就是它子节点需要更新的值先放在父节点里。

当没有查到子节点头上时,父节点就一直欠着子节点的值,因为反正还没有找到子节点,没人会关系他是多少,但是当找到子节点时,父节点就必须把欠的值发下去,不然会引起子节点的不满。这就需要传递懒惰标记。

把欠的值还给子节点,就是下传懒惰标记。

区间修改(区间加上某个值):

void update(int l, int r, int c, int s, int t, int p) {

// [l,r] 为修改区间,c 为被修改的元素的变化量,[s,t] 为当前节点包含的区间,p

// 为当前节点的编号

if (l <= s && t <= r) {

d[p] += (t - s + 1) * c, b[p] += c;

} // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改

if (b[p] && s != t) {

// 如果当前节点的懒标记非空,则更新当前节点两个子节点的值和懒标记值

d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);

b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点

b[p] = 0; // 清空当前节点的标记

if (l <= m) update(l, r, c, s, m, p * 2);

if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);

d[p] = d[p * 2] + d[p * 2 + 1];

同时查询也要变化:

if (l <= s && t <= r) return d[p];

// 当前区间为询问区间的子集时直接返回当前区间的和

if (b[p]) {

d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m),

b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 将标记下传给子节点

b[p] = 0; // 清空当前节点的标记

int sum = 0;

if (l <= m) sum = getsum(l, r, s, m, p * 2);

如果你是要实现区间修改为某一个值而不是加上某一个值的话

d[p] = (t - s + 1) * c, b[p] = c;

d[p * 2] = b[p] * (m - s + 1), d[p * 2 + 1] = b[p] * (t - m),

b[p * 2] = b[p * 2 + 1] = b[p];

b[p] = 0;
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

追梦_赤子

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

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

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

打赏作者

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

抵扣说明:

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

余额充值