crazycloudcc@blog:~/learn/segment-tree — zsh — 80×24
--:--:--···
crazycloudcc@blog:~/learn/segment-tree$ pwd
/home/crazycloudcc/blog/learn/segment-tree
> cd ./learn/segment-tree

算法 · 进阶 · 前置:数组、区间与二叉树 · 约 28 分钟

线段树:把区间拆成可合并的节点

不再试图“减去”一个最小值。用层层对半的区间树,直接拼出查询范围。

目标 [2,7) · 已取 0 块 · 合并 +∞

框宽=区间长度;框内=最小值。橙框 / 下一步,绿框 / 已取或已更新,虚线 / 已跳过。

二叉区间分解与最小值合并根覆盖[0,8),每个父区间等分为两个不重叠孩子,直到单元素叶子;横向位置对应数组下标,纵向深度对应拆分次数。[0,8)-4[0,4)-2[0,2)-2[0,1)5[1,2)-2[2,4)3[2,3)7[3,4)3[4,8)-4[4,6)-4[4,5)6[5,6)-4[6,8)2[6,7)2[7,8)8下标01234567已取T[parent] = min(T[left], T[right])

0/9 · [0,8) 仅部分相交,拆成左右孩子,继续判断。

滑块支持方向键和 Home / End;边界越过另一端时会带动另一端,始终保持 l≤r。改变输入或模式会重置步骤。赋值每轮从原数组开始,不累计。减少动画模式关闭播放,单步仍可用。

同一步,对照算法

query(node, l=2, r=7)
  if disjoint: return +∞
  if fully covered: return node.min
  return min(query(left), query(right))

// 下一动作:[0,8) · split
// 已取 0 块,合并值 +∞

这是递归算法的教学伪代码;图中事件日志为了回放而预先生成,不把日志生成的开销算作单次操作复杂度。

先想一想:最小值从 −4 改成 9,祖先可以都加 13 吗?

不行。只给叶子赋值,再逐层取左右孩子的 min。[4,6) 从 −4 变成6,[4,8) 变成2,全局最小值变成−2;各层变化不等于同一个增量。

再进一步:带着累积和在线段树上找边界

独立的求和舞台,不沿用上面的有符号 min 树。给定非负数组,从 L 向右找第一个使累计和达到 t 的元素。max_right 返回的是仍满足 sum<t 的最大半开边界 R;若 R<n,失败元素正是 A[R],不在已接受区间里。

修改会保留当前数组并重新开始搜索;这里只观察修改后的搜索路径,不逐步演示更新操作。所有数值非负,阈值必须大于 0。当前 A=[2, 0, 3, 1, 4, 0, 2, 1]。

已接受 [0,0) · acc=0 < t=7

框内=区间和,框宽=区间长度;橙框=下一动作,绿框=已接受,虚线=起点左侧跳过。下方条带随接受的整块向右增长。

整块接受与失败下钻树节点保存非负区间和;从左向右接受完整节点,若加入后达到阈值则拆开,直到找到不能接受的叶子。下方绿色带表示已接受半开区间,橙色边界指针随步骤移动。[0,8)13[0,4)6[0,2)2[0,1)2[1,2)0[2,4)4[2,3)3[3,4)1[4,8)7[4,6)4[4,5)4[5,6)0[6,8)3[6,7)2[7,8)1T[parent] = T[left] + T[right]下标01234567绿色带 [0,0) 的和 = 0,下一候选从下标 0 开始接受整块时跳过子树;失败时才沿孩子下钻。

边界搜索 0/5 · 0+13≥7:整块装不下,向下拆开,先左后右找第一片失败叶子。

滑块支持方向键、Home / End;按钮支持 Tab 与 Enter。切换输入/预设停止播放并重置步骤,上一步可回看;减少动画模式不自动播放。绿色带和橙色指针的移动只是状态过渡,不表示接受了尚未完成的动作。

与图同步的搜索步骤

max_right(L=0, sum < 7)
  acc = 0
  visit(node):
    if node.r <= L: skip
    if L <= node.l and acc + node.sum < t:
      acc += node.sum; R = node.r; return
    if leaf: return node.l and stop search
    visit(left); if not found: visit(right)

// 下一动作:split
// 已接受 [0,0),acc = 0
// 完成当前动作后,图与累积和同步更新

预先建树及生成教学日志需 O(n) 空间与建树时间;在已维护的树上,一次标准边界搜索为 O(log n),不是对每个二分中点再做一次 O(log n) 查询。

为什么不能直接把上面的负数数组搬过来?

因为 sum<t 必须随右边界扩展只从真变假,不能再变真。例如 [5,−6,7]、t=4,前缀和 0、5、−1、6 的判断为真、假、真、假。非负输入保证本例单调;一般 max_right 也可使用别的合并规则和单调谓词,并不是一概禁止负数。

看完图,带走这些结论

线段树把数组区间反复分成不重叠的左右两半。叶子保存一个元素,内部节点保存两个孩子的最小值。查询把目标区间拆成若干完整节点,再取这些节点的最小值;单点赋值只需修改叶子并沿祖先重新合并。

适用条件与边界

本课用零基左闭右开[l,r),0≤l≤r≤n;数组无需有序。演示固定八个有正有负的整数,每次赋值从原数组开始,不累计上轮操作。数学模型支持1到16项、每项−99到99的整数,包含奇数长度且不补叶。空区间没有有限最小值,以+∞作为min的单位元返回;它不是数组中的一个值。主舞台讲点赋值与区间最小值;第二个独立舞台用非负数组演示求和阈值边界搜索。两者都不包含区间修改或懒标记。第二舞台固定8项0到9,阈值1到80,改值保留在当前数组中;底层边界模型允许1到16项0到99及正安全整数阈值。空数组不在本教学模型支持域,L=n的空后缀支持。

始终保持的规律

每个节点保存它所覆盖区间的最小值,父节点=min(左孩子,右孩子)。查询取出的完整节点互不重叠,最终恰好覆盖目标[l,r)。

逐步例子

  1. A=[5,−2,7,3,6,−4,2,8]。根节点覆盖[0,8),分为[0,4)和[4,8);一直分到[i,i+1)叶子。内部节点的值等于两个孩子的min,所以根保存−4。
  2. 查询[2,7):[0,8)只部分相交,继续分。[0,2)不相交,返回+∞;[2,4)完全包含,直接取3,不必再看它的叶子。
  3. 右半边取[4,6)的−4,以及[6,7)的2;[7,8)跳过。三个完整块[2,4)、[4,6)、[6,7)互不重叠且恰好覆盖[2,7),结果min(3,−4,2)=−4。
  4. 正确性:不相交节点对目标无贡献;完全包含时该节点已保存整段min;部分相交时,左右孩子是不重叠的完整分割,递归后取min不会丢失元素。每层最多两条边界路径继续拆分,其他完整块直接停止。
  5. 把A[5]从−4赋值为9:先改[5,6)叶子,再重算[4,6)=min(6,9)=6、[4,8)=min(6,2)=2、[0,8)=min(−2,2)=−2。不能给所有祖先都加13,也不能只把祖先与9取min,否则旧的−4永远删不掉。
  6. 奇数长度也能建树:区间[L,R)长度大于1时,以M=floor((L+R)/2)切成[L,M)与[M,R),两边非空且长度更小。不要求n为2的幂,本课递归结构不填充虚拟元素。
  7. 边界搜索例子:换用独立非负数组A=[2,0,3,1,4,0,2,1],L=0,t=7。节点存sum而非min。先接受[0,4)的整块和6;加入[4,8)会达到阈值,继续拆开,最终A[4]=4不能加入。max_right返回R=4:已经接受的是[0,4),首次失败元素下标也是4,但不包含在绿色区间里。
  8. 边界搜索不变量:acc始终等于已接受连续区间[L,R)的和。起点左侧跳过;整块相加仍小于t就接受并跳过子树,否则先左后右下钻,找到失败叶后立即停止。非负数使谓词sum<t随右端扩展只能从真变假,所以不会错过更早失败位置。t>0保证空段sum=0满足谓词。
  9. 边界与无解:若首项已经达到阈值,返回R=L,接受空段;零值仍可接受;若剩余全部元素仍不足t或L=n,返回R=n,这不是可访问的元素下标。若题目要一基的首次达标位置,答案为R+1,无解按该题约定为n+1。
  10. CSES Dynamic Range Minimum Queries采用一基闭区间[a,b],应转换为本课零基[a−1,b);赋值位置k转换为k−1。原题值为正数,本课用负数只是展示min同样适用,不是原题约束。

复杂度、合并规则与数值边界

自底向上建树O(n),保存2n−1个有效节点,空间O(n)。树高O(log n),点赋值只沿一条祖先链,区间查询每层只沿左右边界继续拆分,二者O(log n),假设一次min为O(1)。可推广到满足结合律且有单位元的运算,例如和的单位元0、最小值的单位元+∞;不要求可逆。若运算不交换,仍必须按左到右顺序合并。边界搜索在已有求和树上直接跳过整块并沿失败路径下钻,标准实现O(log n),避免外层二分配合区间查询的O(log² n)。本例sum<t必须非负且t>0;这不是所有max_right谓词的统一取值限制。最大模型总和为16×99=1584,安全整数内精确。交互模型会重新建树并生成回放日志,以小数组换取每步可见;这不代表演示组件本身每次渲染为O(log n)。CSES值≤10⁹,min不做求和,不会产生更大的值;C++可用numeric_limits<long long>::max()作哨兵,必须保证不小于合法值,别拿0当最小值单位元。

常见误区

  • 将两个前缀最小值相减:例如[1,8,6]与[1,9,7]的前缀最小值全相同,却有不同的后两项最小值。min没有求和那样的逆运算。
  • 只判断节点与目标有交集就拿整块:节点可能包含目标之外的更小值。只有完全被目标包含时才能停止下钻。
  • 把点赋值当点增加:set(p,u)直接替换原值,父节点必须从两个孩子重新求min;旧最小值变大时尤其不能只做min(old,u)。
  • 混淆[l,r)与[l,r]:右端r不取。空区间l=r的返回值+∞表示合并单位元,不声称其有有限最小值。
  • 把树形图的框宽当最小值大小:宽度只编码覆盖元素的数量,框内数字才是该段的最小值。
  • 本课没有懒标记:给一整段赋值或加数需要额外设计,不能直接套单点赋值路径。

自测与答案

查询整个数组需要下钻所有叶子吗?空区间返回0正确吗?把A[5]从−4改成9以后,全局最小值是多少?

不需要,根完全被包含,直接读取根。空区间不能返回0,否则正数区间合并时会错误得到0;本课返回+∞。赋值后全局最小值是−2,需要沿叶子到根重算,而不是给祖先统一加13。

进阶 FAQ:满足结合律,为什么仍不能交换左右?

结合律只允许改变括号,不允许改变元素次序。min 恰好还满足交换律,因此左右交换看不出区别;线段树的一般合并规则不能依赖这个巧合。下面是静态延伸例子,第一个主舞台演示区间最小值与点赋值,第二个舞台演示求和阈值边界搜索;这里不把逆序对当成已经实现的交互。

设数组值只可能是 0、1、2。逆序对是满足 i<j 且 A[i]>A[j] 的位置对;相等值不计入。每段保存 (c₀,c₁,c₂,inv):三个值的出现次数,以及段内逆序对数。空段摘要为 (0,0,0,0),是合并单位元。

把左段 L 接在右段 R 前面,各频数直接相加。新的逆序对恰分为 L 内部、R 内部、跨界三类,所以 inv(LR)=inv(L)+inv(R)+c₁(L)c₀(R)+c₂(L)[c₀(R)+c₁(R)]。跨界项只数左值大于右值的组合,不包含相等值。

例如 L=[2,1]、R=[0,1]。左段内部有 1 对,右段内部为 0;跨界有 1×1+1×(1+1)=3 对。因此 LR=[2,1,0,1] 的逆序对数为 4。交换左右得到 RL=[0,1,2,1],却只有 1 对。频数相同,不代表逆序对数相同。

为什么仍有结合律?(L 合并 M) 合并 R 与 L 合并 (M 合并 R) 都是同一串 LMR 的摘要。每一对位置要么同属一段,要么跨越段界,总被恰好计入一次;改变括号不会改变位置先后。但交换成 RML 已是另一串,不能用结合律证明答案相同。

在固定三个取值的前提下,摘要大小与一次合并都为 O(1),因此点赋值与区间查询仍可 O(log n)。查询必须按从左到右的顺序合并;迭代查询若使用左右两个累积器,新取出的右侧块要放在右累积结果前面,最后再把左累积结果接到右累积结果前面。

实现时频数乘法和 inv 都要使用足够宽的整数,并在乘法前完成宽类型转换。一般长度 n 的数组至多有 n(n−1)/2 对;n=200000 时这个通用上界为 19,999,900,000,已超过有符号 32 位整数。这里仅用通用上界说明类型选择,不声称只有三个值时一定能达到它。

这不是任意区间更新模板:本例只说明顺序敏感的合并与点赋值。若要给整段替换值或做映射,还需另行设计摘要与懒标记,不能直接套用当前主舞台。相关练习来源是 2026-10-12 已结束的 AtCoder 算法讲习会比赛,不代表今天的新热点或可核实搜索热度。

参考资料

纯文字 Markdown 阅读版

branch: mainposts: 12encoding: utf-8