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