【数据结构】线段树初步

  众所周知,线段树是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.

原文地址:https://www.cnblogs.com/Melacau/p/Segment_Tree.html