• 【数据结构】线段树初步


      众所周知,线段树是OI当中十分重要的一个数据结构,所以我们今天就来讲一讲这个线段树。  

      首先,我们来了解一下什么是线段树。给定一个1~ n的区间,我们考虑,将这个区间进行二分,使得这个区间下拥有两个小区间,如此反复操作,直至这个区间被二分的只剩下一个点,那么这就是这棵线段树的叶节点,线段树的实质上是一颗二叉树,但未必是一颗完全二叉树。

      那么,引入这样的线段树结构有什么用呢?   我们可以发现,对于线段树上的一个结点,其父节点的信息一定是它和它兄弟的综合,也就是说,我们查询一个区间内的信息,就只需要遍历到一个(或多个的综合)代表着该区间的结点,就可以得到我们想要的信息,而不需要遍历线段上的每个点,显然这样的效率是会大大优化的,通过理论证明,我们可以发现,对于一棵线段树,他的每次操作的时间复杂度是O(qlog2(n))的,而它的最大空间复杂度可以近似看为O(4n),事实上可以证明,实际的空间复杂度约为O(大于等于2n的最小的2的整数次方)。这样的空间复杂度是较高的,因此我们常常使用离散化的方式降低线段树的空间复杂度。 

      接下来我们讲一下如何进行编程实现。

      首先,我们考虑如何进行定点修改,显然对于一次修改,我们可以直接通过从整条线段上一直进行二分,找到它所对应的结点,再进行修改,之后回溯修改它父亲结点的信息即可,这样的效率显然是O(log2(n))的。(标程略)

      接下来我们来考虑如何查询区间内的信息,对于一个区间,显然要么它恰好是线段树上的一个区间,要么它就是由多个相邻区间拼凑而成,因此我们同样考虑进行递归回溯,使得这个区间内的所有子区间被查询到且不重复,进行合并处理的出最后答案,事实上这样的效率也是O(log2(n))的。(附简单标程,pushdown会在后文中提到:))

    1 query(int l,int r,int a,int b,int t){//查询某区间的值 
    2 //[l,r]是我们所要查询的区间,[a,b]是当前所递归到的区间
    3     if (a==l&&b==r) return tree[t].val;
    4     pushdown(t,a,b);
    5     int m=(a+b)/2;
    6     if (r<=m) return query(l,r,a,m,t*2);
    7     if (l>m) return query(l,r,m+1,b,t*2+1);
    8     return query(l,m,a,m,t*2)+query(m+1,r,m+1,b,t*2+1);
    9 }
    点此查看查询函数query

      现在我们继续进行拓展,要如何进行区间修改呢?

      我们考虑如果仅仅是按照定点修改的方法进行处理,每次操作的复杂度O(nlog2(n)),这样显然是复杂度过高不可接受的。

      考虑采用类似查询的操作进行处理,这时出现了一个问题,我们只能更改到区间的值,如果这时我们查询了这个区间的子区间的值,显然这会导致我们的解不是当前状态下的解,因此,我们考虑将区间更改的值进行延迟传递,这就是我们常常听到的线段树的lazy标记(我个人习惯称之为mark)。 这里的延迟传递是什么概念呢?

      我们可以这么认为,在进行一个操作时,只要我所递归到的区间的值是当前状态即可,否则我就没有更新的必要,因此,我们将每次要进行更新的值暂时存在一个已经访问到的区间,只有我递归到了它的子区间,我才需要进行值得更新传递,这也就是我们所说的lazy标记传递。(附上线段树延迟标记传递的函数)

     1 void pushdown(int t,int l,int r){//传递延迟标记 
     2 //t是当前区间标号,[l,r]是当前区间
     3     if (l==r||!tree[t].mark) return;
     4     int x=r-l+1;
     5     tree[t*2].val+=(x-x/2)*tree[t].mark; 
     6     tree[t*2+1].val+=(x/2)*tree[t].mark;
     7     tree[t*2].mark+=tree[t].mark;
     8     tree[t*2+1].mark+=tree[t].mark;
     9     tree[t].mark=0;//清空当前标记 
    10 }
    点此查看延迟标记的传输函数pushdown

      接下来,我们来看一下如何进行区间修改(附相关函数)

     1 update(int a,int b,int l,int r,int t,int add_val){   //进行更改 ,a,b表示当前区间的端点;l,r表示更改区间的端点; 
     2     int x=b-a+1;
     3     if (l<=a&&r>=b){
     4         tree[t].mark+=add_val;
     5         tree[t].val+=x*add_val;
     6         return;
     7     }
     8     pushdown(t,a,b);
     9     int m=(a+b)/2;
    10     if (m>=l) update(a,m,l,r,t*2,add_val);
    11     if (m<r) update(m+1,b,l,r,t*2+1,add_val);
    12     tree[t].val=tree[t*2].val+tree[t*2+1].val;
    13 }
    点击查看修改函数update

      于是我们就讲完了线段树的一些基本操作了,撒花~~

     本文由Melacau编写,Melacau代表M星向您问好,如果您不是在我的博客http://www.cnblogs.com/Melacau上看到本文,请您向我联系,email:13960948839@163.com.

  • 相关阅读:
    jQuery选择器
    js创建对象的几种方式
    call()和apply()
    js实现二分查找
    关于HTTP请求
    node/npm命令收集
    微信H5开发笔记——记录开发中用到的知识(手机摇动、事件捕获、wechat禁止分享、正则、promise...)
    css额外学习笔记
    gulp 结构和运行步骤——笔记1
    手机版H5开发--收集大神们的笔记
  • 原文地址:https://www.cnblogs.com/Melacau/p/Segment_Tree.html
Copyright © 2020-2023  润新知