算法学习——线段树

本文详细介绍了线段树的概念、如何将数组转换为线段树以及其实现过程,包括区间求和、区间修改和查询的操作,特别提到了使用懒惰标记优化区间修改性能的方法。
摘要由CSDN通过智能技术生成

线段树不是一颗二叉树,而是利用树的思想的一个数组。

线段树常常用来维护区间信息,可以在O(logN)的时间复杂度内完成单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。

假设我们有一个长度为5的数组a : {10 ,11 , 12 , 13, 14},要将其转换成线段树,会是什么样子呢?

如图便是我们得到的线段树的样子。我们可以从中发现,这棵树的每一个节点都存储了一个区间内的求和信息。如d[4],存储的就是下表为0,1的元素10+11的和,即为21。并且我们还能观察到,每个节点的左字节点的序号是父节点的2倍,同时右子节点的序号则是2倍+1。据此,我们可以将一个区间划分成一颗线段树了。但是没个区间长度该怎么划分呢?很简单,一半一半。

左子节点区间是父节点区间[s,t]的[s,(s+t)/2],右子节点区间是父节点的[(s+t)/2+1,t]。直到区间长度为1为止。据此,我们实现了一个数组的线段树的构建。

线段树的构建:

有了线段树,我们就可以查询区间信息,修改单点或者区间的值。下面来看看代码怎么实现构建线段树吧。

void build(int s, int t, int p) {
  // 对 [s,t] 区间建立线段树,当前根的编号为 p
  if (s == t) {
    d[p] = a[s];
    return;
  }
  int m = s + ((t - s) >> 1);
  // 移位运算符的优先级小于加减法,所以加上括号
  // 如果写成 (s + t) >> 1 可能会超出 int 范围
  build(s, m, p * 2), build(m + 1, t, p * 2 + 1);
  // 递归对左右区间建树
  d[p] = d[p * 2] + d[(p * 2) + 1];
}

如果区间长度为1,递归返回。 否则我们分别对两边进行递归。同时别忘记在递归的过程中记录信息:d[p] = d[p * 2] + d[(p * 2) + 1];

线段树的查询:

如果我们需要查询一个区间内元素的和,我们可能有以下情况:

1️⃣ 查询的区间在线段树内存在,如查询[1,5]的和,直接返回d[0]即可

2️⃣查询的区间在线段树内不直接存在,如查询[3,5]的和,我们就需要分两段来查询。

首先我们需要查询[3,3]的值,然后我们需要得知[4,5]的值,将两个区间的和相加即可得到最终结果。

在代码实现上面,我们是这样做的:

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 - s) >> 1), sum = 0;
  if (l <= m) sum += getsum(l, r, s, m, p * 2);
  // 如果左儿子代表的区间 [s, m] 与询问区间有交集, 则递归查询左儿子
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  // 如果右儿子代表的区间 [m + 1, t] 与询问区间有交集, 则递归查询右儿子
  return sum;
}

首先如果当前的区间在目标区间当中,我们直接将当前值返回。

否则我们将当前区间一分为2,如果左区间与目标区间有交集,我们直接将左区间递归查询,将sum值不断累加,直到区间信息完全在内。

如果右区间与目标区间有交集,我们直接将右区间递归查询,同理。注意此处左区间每次递归时节点编号都是直接乘2,右区间则是乘2+1.

线段树的区间修改与懒惰标记

当我们需要对于线段树的一个区间内的信息进行修改时,我们会发现这样做的时间复杂度显然会比较高。因此我们需要引入懒惰标记。

什么是懒惰标记呢?简言之,就是先记上帐,后面再去处理。

原本的线段树长这样,我们给[3,5]区间内的元素都加5,显然我们需要给[3,3] , [4,5]都进行加操作。我们直接在两个节点上操作,打上标记。

至于他们的儿子们吗,就慢慢等待以后的更新了。

在后面的操作中,如查询[4,4]区间的值的过程中,我们会先找到[3,5]。但是还带着标记呢,我们就在这时把标记下放,变成了:

代码实现(有标记的前提下):

这个update方法的作用是下放区间的值。第一个if在添加标记值的同时,将区间长度内的所有元素加上了标记值。

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;
    return;
  }  // 当前区间为修改区间的子集时直接修改当前节点的值,然后打标记,结束修改
  int m = s + ((t - s) >> 1);
  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 (b[p] && s != t)则是判断[s,t]区间是否有子节点,同时标记值是否有效(不是0)

区间求和:

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 - s) >> 1);
  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);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}

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

void update(int l, int r, int c, int s, int t, int p) {
  if (l <= s && t <= r) {
    d[p] = (t - s + 1) * c, b[p] = c;
    return;
  }
  int m = s + ((t - s) >> 1);
  // 额外数组储存是否修改值
  if (v[p]) {
    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];
    v[p * 2] = v[p * 2 + 1] = 1;
    v[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];
}

int getsum(int l, int r, int s, int t, int p) {
  if (l <= s && t <= r) return d[p];
  int m = s + ((t - s) >> 1);
  if (v[p]) {
    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];
    v[p * 2] = v[p * 2 + 1] = 1;
    v[p] = 0;
  }
  int sum = 0;
  if (l <= m) sum = getsum(l, r, s, m, p * 2);
  if (r > m) sum += getsum(l, r, m + 1, t, p * 2 + 1);
  return sum;
}
  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值