学习线段树总结
这几天在复习 qbxt 的知识,学到了线段树,就来总结一下。
线段树
上面这张图显然是线段树,线段树就是一个处理区间的一个数据结构,将整个线段划分成一个树的结构,将长度不是 1 的段划分成两个子区间来求解,通过合并两个区间的信息来求解,这也是一个高效的数据结构。
总体时间复杂度为 \(O(\log n)\)
- 适用范围:区间的最小值或最大值,区间的修改,区间求和
操作
在所有操作开始之前,可以观察到一个根节点为 \(p\) 的左儿子为 \(p^2\) ,右儿子为 \(p^2+1\)
1 |
|
建树
我们可以想到像树上 DFS 一样,可以一直访问儿子节点,直到儿子节点长度为 1 ,我们可以通过数据上的值去更改子节点的值,再有子节点来合并(更新)父节点的值。
1 |
|
区间查询
首先,我们可以想到还是像建树一样递归求解,如果这个区间在所要求的区间上了,直接返回,如果左儿子的子区间和原答案有交集,那么就递归到相应的节点上求解。那么思路就讲好了,代码怎么写呢?下面放上几张图就了解了
设所在区间为 \([l,r]\) , 查询区间为 \([s,t]\) 。
那么根据这张图可以很显然的推出所在区间在查询区间的判断方式:
1 |
|
设中间位置为 \(mid = \dfrac{(l+r)}{2}\) 。
可以很简单的看出,进入左儿子的条件是:
1 |
|
进入右儿子的条件是:
1 |
|
综上,我们可以得出查询代码:
1 |
|
单点修改
在讲区间修改之前,我先来讲一下最基础的单点修改。单点修改也是递归找需要更改的点,然后再返回合并父节点的值
1 |
|
区间修改
懒标记
简单来说,就是通过延迟对节点信息的更改,从而减少可能不必要的操作次数。每次执行修改时,我们通过打标记的方法表明该节点对应的区间在某一次操作中被更改,但不更新该节点的子节点的信息。实质性的修改则在下一次访问带有标记的节点时才进行。
----- OI-Wiki
这段话深刻的阐释了懒标记的作用,如果不用懒标记时间复杂度将会达到
\(\mathcal{O}(n \log n)\)
有点慢,这样一来时间复杂度为 \(\mathcal{O}(\log n)\) 的。
听我这样一讲有点蒙先来看代码吧:
1 |
|
举个例子吧:
如果我想要更改 \([9,10]\) 的每个数加上 5,我们可以先直接在这个区间上改,并给它们打上标记。
虽然,子节点没有更新值,但当我们要查询这两个子节点的信息时,我们会利用标记修改这两个子节点的信息,使查询的结果依旧准确。
现在,来查询一下 \([9,9]\) 的值,当递归到 \([9,10]\) 时,因为懒标记不为 0 ,所以将该区间的子区间更新并清零。
当然为了编写的简单性,修改懒标记可以写一个单独的函数
pushdown
。
带乘法的查询也同理,只是新加一个乘法懒标记而已
带懒标记的查询
1 |
|
习题
基本上有了这些操作就可以写以下几道题目:
x 1// 初始化23for(int i=1;i<=n;i++)4 fa[i] = n+i;5 6for(int i=n+1;i<=n+n;i++)7 fa[i] = i,sz[i] = 1,sum[i] = i-n;89// 第二个操作,仅仅将一个节点移动到另一个集合上。1011int u = find(x),v = find(y);12if(u != v){13 fa[x] = v;14 sz[u] --;sz[v] ++;15 sum[u] -= x;sum[v] += x;16}cpp
这道题是个纯模板题
-
这道题也差不多是个模板题,只是要注意:先乘后加 懒标记
-
单点修改的模板
-
单点修改,查询最大值的模板
HDU-1394 Minimum Inversion Number
线段树解决逆序对问题
POJ-3468 A Simple Problem with Integers
区间修改的模板题
模板题/好题未完待续 \(\dots \dots\)
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!