众所周知,线段树是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 }
现在我们继续进行拓展,要如何进行区间修改呢?
我们考虑如果仅仅是按照定点修改的方法进行处理,每次操作的复杂度是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 }
接下来,我们来看一下如何进行区间修改(附相关函数)
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 }
于是我们就讲完了线段树的一些基本操作了,撒花~~
本文由Melacau编写,Melacau代表M星向您问好,如果您不是在我的博客http://www.cnblogs.com/Melacau上看到本文,请您向我联系,email:13960948839@163.com.