线段树模板

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

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

线段树:

  • 定义:线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 对于线段树中的每一个非叶子节点[a,b],它的左儿子表示的区间为[a,(a+b)/2],右儿子表示的区间为[(a+b)/2+1,b]。因此线段树是平衡二叉树,最后的子节点数目为N,即整个线段区间的长度。

  • 作用 :广泛应用于区间修改和区间查询

  • 结构:如下图1-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.线段树数组一般开原数组的四倍大小即够用。

线段树模板_第2张图片

图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

这样,如果我们要修改图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));
}

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

你可能感兴趣的:(#,线段树)