线段树模板

先给出一个很裸的线段树模板题:给出n个数,依次为a[1],a[2]…a[n],接下来有m次操作。每次让你执行两个操作:1.将区间[L,R]内的所有数+k。2.查询[L,R]范围内所有数的总和。

遇到这种题,我们就可以用线段树解决。(暴力会超时,我之前写的树状数组博客中提到过)至于线段树比较快的原因,我就不多提了 (很多博客都有提及) ,总之它的区间修改以及区间查询都是log级别的时间复杂度。

线段树:

  • 定义:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。
  • 作用 :广泛应用于区间修改和区间查询
  • 结构:如下图1-1
    在这里插入图片描述
    图1-1
  • 性质:

  1. 线段树结点标号(如下图1-2):从上至下,从左至右依次标号,例如上图中,区间[1,8]标号为1,区间[1,4]标号为2,区间[5,8]标号为3…显然,若当前结点下标为k,那么该结点左儿子的下标为2*k,右儿子的下标为2*k+1
  2. 父亲结点与儿子节点的表示范围关系:若父亲结点表示范围为[l,r],设mid=(l+r)/2,那么左儿子的表示范围为[l,mid]、右儿子的表示范围为[mid+1,r]。
  3. 线段树最下面一层存储的是原数组的信息。
  4. 线段树数组一般开原数组的四倍大小即够用。

在这里插入图片描述

图1-2

代码实现:

定义:

首先开一个结构体,用来存储线段树中每个结点的信息(包含的数据范围,对应的值,包括下文要提到的懒标等)

#define MAX 100010 //数据范围
int a[MAX];        //原数组
struct node
{
   int l, r, k, sum, lazy;
} tr[MAX * 4]; //线段树数组

然后我们还需要一个更新函数,例如我们更改了图1-2中的结点6,那么在他之上的结点3和结点1也肯定要更改,这是必不可少的,又因为父亲结点的值=左儿子的值+右儿子的值,所以这里的更新函数就出来了

void update(int k)
{
  tr[k].sum = tr[k * 2].sum + tr[k * 2 + 1].sum;
}  
建树

要使用线段树肯定得先建好线段树,这里我们用递归修建线段树

void build(int k, int l, int r) //当前节点编号为k,他表示的区间[l,r]的和
{
  tr[k].l = l; //先赋值区间范围
  tr[k].r = r;
  if (l == r) //如果这个区间是一个点,那说明已经递归到最底下一层,自然就是原数组
  {
      tr[k].sum = a[l];
      return;
  }
  int mid = (tr[k].l + tr[k].r) >> 1; //将父亲区间分割成左区间和右区间
  build(k * 2, l, mid);               //递归建左儿子和右儿子
  build(k * 2 + 1, mid + 1, r);
  update(k); //递归到底层之后即可往上回溯修建各个父亲结点
}
区间修改

要修改一个区间,首先得找到一个区间。那么如何找一个区间呢?我们采取的方法是每次都从线段树顶开始找,也就是编号为1的结点,因为他肯定包含了所有区间情况。假设要修改的区间是[L,R],当前节点k代表的区间范围是[l,r],mid=(l+r)/2。每一次我们遍历一个节点的时候,将[L,R]和[l,r]比较,只会有三种情况:

  1. 区间[L,R]完全在节点k的左儿子包含范围内,也就是R<=mid,这时,我们只需往左递归即可。
  2. 区间[L,R]完全在节点k的右儿子包含范围内,也就是L>mid,同理,我们只需往右递归即可。
  3. 最后一种情况是难点,即L<=mid<R,这时我们要修改的区间横跨了节点k的两个儿子。这时,我们必须把区间分开处理。那应该怎么分割呢?当然是从mid处切开,将要修改的区间一分为二,变成[L,mid]和[mid+1,R],分别递归到左儿子和右儿子。

这样,如果我们要修改图1-1中的区间[5,7],那么我们最终可以找到图1-2中编号为6和14的结点。修改这两个区间就可以完成区间[5,7]的修改,问题来了,在节点6之下还有节点12,13,这两个肯定也要修改。但是如果每次区间修改都改到最底下一层的话,这样的区间修改复杂度比直接在原数组上修改还要高。如何解决呢?

懒人标记!

我们修改了节点6之后先不着急修改下面的结点12,13,(因为我们暂时用不到,没必要修改)而是在节点6上做一个标记,表示这下面需要修改,但是我们还没做!等下次要用到结点12,13了再修改

void change(int k, int l, int r, int y) //当前所在结点为k,我们要将区间[l,r]的值加上y
{
    if (tr[k].l == l && tr[k].r == r) //找到了要修改的区间,标记上懒标,修改当前区间值,直接返回
    {
        tr[k].lazy += y;
        tr[k].sum += (tr[k].r - tr[k].l + 1) * y;
        return;
    }
    pushdown(k); //下传懒标,保证所有遍历到的结点都是更新过的值
    int mid = (tr[k].l + tr[k].r) >> 1;
    if (r <= mid)
        change(k * 2, l, r, y);
    else if (l > mid)
        change(k * 2 + 1, l, r, y);
    else
    {
        change(k * 2, l, mid, y);
        change(k * 2 + 1, mid + 1, r, y);
    }
    update(k); //记得更新
}
void pushdown(int k) //下传懒标
{
  if (tr[k].l == tr[k].r) //如果当前结点已经是最底下一层,那么直接清除懒标即可
      tr[k].lazy = 0;
  if (tr[k].lazy) //如果当前结点有懒人标记,那么把懒标传递给两个儿子,并且修改两个儿子的值
  {

      tr[k * 2].sum += tr[k].lazy * (tr[k * 2].r - tr[k * 2].l + 1);
      tr[k * 2 + 1].sum += tr[k].lazy * (tr[k * 2 + 1].r - tr[k * 2 + 1].l + 1);
      tr[k * 2].lazy += tr[k].lazy;
      tr[k * 2 + 1].lazy += tr[k].lazy;
      tr[k].lazy = 0; //传递之后记得清除当前结点的标记
  }
}
区间查询

区间修改的思路理解了,区间查询也很容易理解

int query(int k, int l, int r) //查询区间[l,r]
{
  if (tr[k].l == l && tr[k].r == r)
      return tr[k].sum;
  pushdown(k); //别忘记下传懒标,保证所有遍历到的结点都是更新过的值
  int mid = (tr[k].l + tr[k].r) >> 1;
  if (r <= mid)
      return query(k * 2, l, r);
  else if (l > mid)
      return query(k * 2 + 1, l, r);
  else
      return (query(k * 2, l, mid) + query(k * 2 + 1, mid + 1, r));
}

以上就是线段树的模板,要想真正理解应该也要花点时间,这样做一些拓展题的时候才可以比较灵活的应用~

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hesorchen

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

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

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

打赏作者

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

抵扣说明:

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

余额充值