zoukankan      html  css  js  c++  java
  • 数据结构之线段树入门

    一、前言

    对于维护区间连续和问题,我们已经学了很多种算法和数据结构,在规定n<=100000,m(操作数)<=200000,内,暴力算法可以解决单点修改,单点求值。前缀和算法可以解决区间求和问题,而最近学的树状数组可以解决单点修改,区间求和的问题。而当我们需要区间修改时,上边的三种算法都将失效,我们亟待引入一个新的数据结构——线段树。

    二、线段树基本思想

    1.线段树是一棵二叉树,每一个子节点存取的是一段区间所需要维护的信息,如最大值,最小值,或区间和。

    2.线段树基本思想:分治

    3.线段树每个节点都以结构体的方式存储,这个结构体有4个属性:左端点、右端点、所维护的信息以及LazyTag(后面会详细说明)

    下图很好地阐释了线段树储存信息的方式:

    当我们需要求1-3区间和时,只需要调取1-2段和3段的信息即可,具体如何调取将会在以下说明。

    注:以下维护信息均为区间和。

    三、线段树五大基本操作之一 —— 建树

    线段树的建树过程实际上是自底向上计算初始值的过程,从顶向下递归,如果是叶子节点那么就输入值,输入完毕后回溯时计算父节点的权值。

    线段树建树代码如下:(注意:一定要把结构体开到4*n级别,手画一棵线段树就知道了!

     1 void build(int l,int r,int k)
     2 {
     3     tree[k].l=l;tree[k].r=r;
     4     if(l==r)
     5     {
     6         scanf("%lld",&tree[k].w);
     7         return;
     8     }
     9     int mid=(l+r)/2;
    10     build(l,mid,k*2);
    11     build(mid+1,r,k*2+1);
    12     tree[k].w=tree[k*2].w+tree[k*2+1].w;
    13 }
    View Code

    四、线段树五大基本操作之二 —— 单点查询

    由于线段树一个非叶节点的两棵子树储存的是这个区间的一半,我们可以根据这个特点每次对范围进行一半的缩小,直到递归到叶子节点为止。时间复杂度为log(n)

    线段树单点查询代码如下:

    1 int query_point(int k)
    2 {
    3     int l=tree[k].l,r=tree[k].r;
    4     if(l==r)return tree[k].w;
    5     int mid=(l+r)/2;
    6     if(x<=mid)return query(k*2);
    7     return query(k*2+1);
    8 }
    View Code

    五、线段树五大基本操作之三 —— 单点修改

    单点修改和单点查询原理一样,只需在回溯时维护一下信息即可。

    线段树单点修改代码如下:

     1 void add_point(int k,int w)
     2 {
     3     int ll=tree[k].l,rr=tree[k].r;
     4     if(ll==rr)
     5     {
     6         tree[k].w+=w;
     7         return;
     8     }
     9     int mid=(ll+rr)/2;
    10     if(x<=mid)add(k*2,w);
    11     else add(k*2+1,w);
    12     tree[k].w=tree[k*2+1].w+tree[k*2].w;
    13     return;
    14 }
    View Code

    六、线段树五大基本操作之四 —— 区间求和

    区间求和依旧是根据线段树的特点,尽可能调用深度较浅的节点,当目前区间被所需区间完全覆盖时,就加上,否则继续递归。

    线段树区间求和代码如下:

     1 int query_interval(int k)
     2 {
     3     int l=tree[k].l,r=tree[k].r;
     4     if(l>=x&&r<=y)
     5     {
     6         ans+=tree[k].w;
     7         return;
     8     }
     9     int mid=(l+r)/2;
    10     if(x<=mid)query(k*2);
    11     if(y>mid)query(k*2+1);
    12 }
    View Code

    六、线段树之LazyTag

    使用线段树的一个重要目的就是进行区间修改,而如果按照单点修改的思路,修改整个区间时将会修改整棵线段树,比朴素算法还劣,这时我们就需要引入LazyTag(懒标记),顾名思义,懒标记十分的懒,只在需要的时候下放,否则就一直呆着。为什么不能一下就全都修改呢?是因为我们其实有很多不需要的信息被下放了,因此造成TLE。因此采用LazyTag改动尽量少点的权值,并改动LazyTag的值,在进行递归的时候为了保证正确性需要先改动子节点的值,该操作叫做标记下放(pushdown),标记下放的时候直接下放到k*2和k*2+1中,并累积到权值和当前节点的LazyTag里(一定要累积,在该节点下放之前父节点可能不止一次下放)注意:在使用LazyTag的程序中,五种基本操作在递归前都需要pushdown!!!

    pushdown代码如下:

    1 void pushdown(int k)
    2 {
    3     tree[k*2].lazytag+=tree[k].lazytag;
    4     tree[k*2].w+=(tree[k*2].r-tree[k*2].l+1)*tree[k].lazytag;
    5     tree[k*2+1].lazytag+=tree[k].lazytag;
    6     tree[k*2+1].w+=(tree[k*2+1].r-tree[k*2+1].l+1)*tree[k].lazytag;
    7     tree[k].lazytag=0; 
    8     return;
    9 }
    View Code

    七、线段树五大基本操作之五 —— 区间修改

    有了LazyTag,区间修改代码不难写出,和区间求和代码思路一样,需要的时候标记下放即可

    线段树区间修改代码如下:

     1 void add(int k,int w)
     2 {
     3     int ll=tree[k].l,rr=tree[k].r;
     4     if(ll>=x&&rr<=y)
     5     {
     6         tree[k].w+=(rr-ll+1)*w;
     7         tree[k].lazytag+=w;
     8         return;
     9     }
    10     if(tree[k].lazytag)pushdown(k);
    11     int mid=(ll+rr)/2;
    12     if(x<=mid)add(k*2,w);
    13     if(y>mid)add(k*2+1,w);
    14     tree[k].w=tree[k*2].w+tree[k*2+1].w;
    15     return;
    16 }
    View Code

    线段树五种基本操作及LazyTag非常重要,请大家一定要好好理解!!

    如果你喜欢我的博客,别忘了点个赞哦~~~

  • 相关阅读:
    【Foreign】数数 [打表][DP]
    【Foreign】猜测 [费用流]
    【Foreign】最大割 [线性基]
    【Foreign】开锁 [概率DP]
    【Foreign】染色 [LCT][线段树]
    【Foreign】阅读 [线段树][DP]
    【Foreign】字符串匹配 [KMP]
    【Foreign】冒泡排序 [暴力]
    【BZOJ1976】能量魔方 [最小割]
    【Foreign】树 [prufer编码][DP]
  • 原文地址:https://www.cnblogs.com/szmssf/p/11042137.html
Copyright © 2011-2022 走看看