Jerry's Blog

Back

线段树的使用场景#

线段树是一种常用来维护 区间信息 的数据结构

​可以在极快的时间内实现单点修改、区间修改、单点查询、区间查询(区间求和,求区间的最大值,求区间的最小值)等操作

线段树的时间复杂度为 O(nlogn)O(nlogn),但具有很大的常数,一般能够适用于 10610^6 数据规模的题.

线段树的空间复杂度为 O(n)O(n),但也具有很大的常数(一般大于 10)

基础线段树#

题目:P3372 线段树 1

如题,已知一个数列 {ai}\{a_i\}nn 项,你需要进行 mm 次下面两种操作:

  1. 将某区间每一个数加上 kk
  2. 求出某区间每一个数的和.

对于 100%100\% 的数据:1n,m1051 \le n, m \le {10}^5ai,ka_i,k 为正数,且任意时刻数列的和不超过 2×10182\times 10^{18}

思路#

​如下图所示,线段树是建立在区间基础上的树,树的每个节点都代表着一段区间【L,R】,之后,在程序中维护每一个节点的信息.

将所有的节点从 112n12n-1 进行标号,则不难得出以下性质:

对于节点 tt,设其区间为 [l,r][l,r],定义 mid=(l+r)÷2mid = (l + r) \div 2,则其子节点有两个,其一为节点 2t2t,其区间为 [l,mid][l, mid],其一为节点 2t+12t+1,其区间为 [mid+1,r][mid+1, r],此外,当且仅当 l=rl = r 时,该节点为叶子节点,没有子节点.

代码实现#

建树#

首先,我们在建树的时候,可以考虑递归建树.

对于每一个节点,如果它是叶子节点,可以直接通过 a 数组设置基础信息,如果其下还有节点,则将该区间从中点处分割为两个子区间,并分别进入左右节点的递归建树,最后将两个子节点的信息 pushup 到父节点,即收集子节点的所有 sum 之和,来刷新父节点的元素和.

额外要注意的是,节点数组需要开至少 4 倍,由于线段树的二分特性,所以线段树的有效节点2n12n-1 ,但是,在下文的一些操作中,可能会访问到叶子节点的子节点,所以至少开 4 倍,这里建议开 8 倍,为了防止不必要的下标越界.

区间修改#

对于一个节点 tt,我们可以尝试实现一个函数,用于让该节点和目标区间重合的部分进行修改.在此题中,我们需要刷新第 tt 个节点,其起屹位置分别为 llrr,使得区间 [x,y][l,r][x,y] \cap [l,r] 中的每一个数增加 kk

首先的首先,我们需要让节点 tt 没有向下处理的所有标记(即记录在 add 数组中的)都推到节点 tt 的两个子节点上,并且要让子节点根据 add 来更新 sum,这个操作叫做 pushdown.更加普遍的,pushdown 就是让一个节点的所有的修改标记都释放给子节点,并且为子结点结算所有受到影响的查询标记.一个节点的修改标记会在施加后立即对查询标记生效,千万不要重复生效.

如果节点 tt 完全包含在了目标区间中(也就是说,[l,r][x,y][l,r]\subseteq[x,y],即满足 lxryl \geq x \land r \leq y),我们就要对该节点的 sum 值进行修正,此节点的每一个值都要增加 kk,则这个节点的 sum 值就要有几个数就加几个 kk,即需要增加的值 Δ=k(rl+1)\Delta = k(r-l+1),按道理来说,我需要将节点 tt 的所有子节点都进行更改,但是先放一放,暂且记在 add 中,作为标记.

如果节点 tt 与目标区间毫不相干(也就是说,[x,y][l,r]=[x,y] \cap [l,r] = \varnothing,即满足 l>yr<xl > y \lor r < x),则无需做任何处理,直接 return

否则,表示节点 tt 与目标区间有重合部分,也间接表明了节点 tt 不是叶子节点,则可以递归处理,最后再将两个子节点的信息 pushup 到父节点.

区间查询#

和修改很类似.

对于一个节点 tt,我们可以尝试实现一个返回值为 int 的函数,用于让该节点和目标区间重合的部分进行修改.在此题中,我们需要查询第 tt 个节点,其起屹位置分别为 llrr,获得区间 [x,y][l,r][x,y] \cap [l,r] 中的每一个数增加 kk

首先的首先,我们需要先进行一次 pushdown,将自己身上撇干净.

如果节点 tt 完全包含在了目标区间中,我们就需要返回该节点中的元素的和,即 sum[t]

如果节点 tt 与目标区间毫不相干,则直接 return 0

否则,表示节点 tt 与目标区间有重合部分,也间接表明了节点 tt 不是叶子节点,则可以递归处理,最后再将两个子节点的信息相加返回.

标程#

带有多种修改和查询操作的线段树#

题目:P3373 线段树 2

已知一个数列 nn 个数,你需要进行 qq 次下面三种操作:

  • 将区间 [x,y][x,y] 每一个数乘上 kk
  • 将区间 [x,y][x,y] 每一个数加上 kk
  • 求出某区间每一个数的和对 mm 取模的结果.

对于 100%100\% 的数据:1n1051 \le n \le 10^51q105,1k104,m=5713731 \le q \le 10^5,1\le k\le 10^4,m = 571373

思路#

对于多种查询的线段树,对每一种查询,给每一个节点设立一个对应的编辑,然后在 pushuppushdown 中分别维护其添加、生效及移除即可.

对于多种改动的线段树,对每一种改动,先找出改动的优先级,然后再找到改动后对其余两种改动的影响.

就像此题,此题有两种改动,加法和乘法,如果对于某一个节点,其乘法标记增加了 kk,那么其加法标记就会增加 xkxk,其中,xx 是原本就有的加法标记数量.

容易发现,乘法对加法的影响很容易确定,但是,如果对于某一个节点,其加法标记增加了 kk,那么我们就很难确定其乘法标记增加多少.(也许是 (x+k)÷k(x+k)\div k,其中,xx 是原本就有的加法标记数量,但却大概率是个小数)

所以,我们在处理加法标记时,最好没有乘法标记(即乘法标记为 1),因此,乘法的优先级就大于加法.

与此同时,如果有一个操作“覆盖”,能进行区间的覆盖操作,那么很容易就可以知道,覆盖操作的优先级是大于乘法的.

因此,我们在 pushup 中只处理子节点提供给父节点的查询标记,而在 pushdown 中依照优先级处理父节点传递给子节点的所有标记及其影响,并清空父节点的标记.

对于此题,每一个节点都有 1 个查询标记 sum 和 2 个修改标记 addmul

标程#

复杂线段树——辅助标记的添加与应用#

题目:P2572 序列操作

题目描述#

lxhgww 最近收到了一个 0101 序列,序列里面包含了 nn 个数,下标从 00 开始.这些数要么是 00,要么是 11,现在对于这个序列有五种变换操作和询问操作:

  • 0 l r[l,r][l, r] 区间内的所有数全变成 00
  • 1 l r[l,r][l, r] 区间内的所有数全变成 11
  • 2 l r[l,r][l,r] 区间内的所有数全部取反,也就是说把所有的 00 变成 11,把所有的 11 变成 00
  • 3 l r 询问 [l,r][l, r] 区间内总共有多少个 11
  • 4 l r 询问 [l,r][l, r] 区间内最多有多少个连续的 11

对于每一种询问操作,lxhgww 都需要给出回答,聪明的程序员们,你们能帮助他吗?

输入格式#

第一行两个正整数 n,mn,m,表示序列长度与操作个数.

第二行包括 nn 个数,表示序列的初始状态.

接下来 mm 行,每行三个整数,表示一次操作.

输出格式#

对于每一个询问操作,输出一行一个数,表示其对应的答案.

数据范围#

对于 100%100\% 的数据,1n,m1051\le n,m \le 10^5

我们想要实现对于每一种修改,每一种查询都能进行变化.

对于覆盖,两种查询都很好求,但对于取反,第二种查询就比较复杂.

首先,为了计算出区间内连续 1 的个数,我们需要知道其子区间从左边开始数的连续的 1 的数量lv1 以及从右边开始数的连续的 1 的数量rv1,那么,其区间内连续 1 的个数 con1 的转换公式为 fa.con1=max(left.con1,right.con1,left.rv1+right.lv1)fa.con1 = max(left.con1, right.con1, left.rv1 + right.lv1),其中节点 fafa 的左右子节点分别为 leftleftrightright

但是,对于 lv1rv1,甚至 con1,也很难知道其取反后的结果,所以,我们为这两个辅助标记再定义辅助标记,我们需要知道其子区间从左边开始数的连续的 0 的数量lv0从右边开始数的连续的 0 的数量rv0 以及区间内 0 的个数con0,所以,对于一次取反操作,我们只需要分别交 lv1rv1lv0rv0con1con0 就可以了.

对于这么多的标记,我们可以定义一个结构体来进行储存,并且重载 + 运算符来表示合并.

最后,我们只需要修改 pushdownpushup 以及所有的改动函数来初始化和维护这些变量就可以了.

标程#

线段树
https://blog.jerrylab.top/blog/ds/seg
Author Jerry
Published at 2026年3月16日