线段树入门
当我们需要维护一个序列且进行一些满足结合律(加、乘、异或、\(\gcd\) 等)的区间操作时可以考虑使用线段树。如果单次运算是 \(O(1)\) 的,那么区间查询和区间修改等操作都是 \(O(\log n)\) 的。
线段树是一棵二叉树,它的每个结点都维护一个区间的信息。根结点对应整个区间 \([1,n]\),若父亲结点维护的区间是 \([l,r]\),则左右儿子分别维护 \([l,\lfloor \dfrac{l +r}{2}\rfloor]\) 和 \([\lfloor \dfrac{l +r}{2}\rfloor+1,r]\) 这两个区间。区间长度为 \(1\) 的结点就是线段树的叶子结点。
显然,线段树的深度 \(h=\lceil \log n\rceil\),总结点个数不超过 \(2^{h + 1}-1\lt 4n\),要开四倍空间。
线段树一般采用堆式存储,下标为 \(p\) 的结点的左右儿子分别为 \(2p\) 和 \(2p+1\),建树时从根结点往下递归,求出左右儿子的区间和后就可以得出父亲的区间和。建树的复杂度事实上是 \(O(n)\) 的,因为只经过每个结点一次。
例如我们对一个序列进行以下两种操作:
- 查询区间 \([l,r]\) 内所有元素之和
- 将区间 \([l,r]\) 内所有元素加上 \(k\)
建树的代码大概是这样的:
def push_up(pos):
# 根据子树信息更新当前区间和
tr[pos] = tr[pos * 2] + tr[pos * 2 + 1]
def build(pos, l, r):
if(l == r):
# 叶子结点
tr[pos] = a[l - 1]
return
mid = (l + r) // 2
build(pos * 2, l, mid)
build(pos * 2 + 1, mid + 1, r)
push_up(pos)
如果没有修改操作,查询区间和非常容易:从根结点开始向下递归,如果某个区间被查询的区间完全覆盖,直接把这个区间的和加入答案并返回;否则与区间中点进行比对,若 \(l\le mid\) 说明部分信息在左子树中,若 \(r\gt mid\) 说明部分信息在右子树中,分别递归进入这些有用的子树即可。在查询操作中,一个长度为 \(n\) 的区间能分裂出的小区间的数量级是 \(O(\log n)\) 的,因此查询的时间复杂度是 \(O(\log n)\)。
如果要对区间 \([l,r]\) 进行修改(全部加上 \(k\)),可以发现最坏情况下需要修改的结点数是 \(O(n)\) 的,这样显然不能接受。但是我们可以注意到一个这样的事实:倘若我们要对某个子树进行修改,而今后的查询最多只会查到这棵子树的某一层,那么再下面的结点就算不更新也不会产生影响了。因此线段树引入了一个 lazy tag,用来延迟对结点信息的修改,这样就能减少修改次数。即每次修改时我们只修改 \(\log n\) 个直接分裂出来的区间,并将它们的 tag 均加上 \(k\),将来如果要修改或查询它们的子树时再将 tag 向下传递。
简单来说,结点上的标记的意义是该区间已被更新,但子区间并没有,标记中记录的就是子区间需要更新的信息。
def push_down(pos, l, r):
mid = (l + r) // 2
# 向下传递标记
tag[pos * 2] += tag[pos]
tag[pos * 2 + 1] += tag[pos]
# 更新两个子区间的信息
tr[pos * 2] += tag[pos] * (mid - l + 1)
tr[pos * 2 + 1] += tag[pos] * (r - mid)
# 清空当前区间的标记
tag[pos] = 0
def add(pos, l, r, x, y, k):
if(x <= l and y >= r):
# 整个区间都被修改
tr[pos] += k * (r - l + 1)
tag[pos] += k
return
# 标记下传
push_down(pos, l, r)
mid = (l + r) // 2
if(x <= mid):
# 左边有一段要改
add(pos * 2, l, mid, x, y, k)
if(y > mid):
# 右边有一段要改
add(pos * 2 + 1, mid + 1, r, x, y, k)
# 更新当前区间的和
push_up(pos)
def query(pos, l, r, x, y):
if(x <= l and y >= r):
# 整个区间参与贡献
return tr[pos]
# 标记下传
push_down(pos, l, r)
mid = (l + r) // 2
ret = 0
if(x <= mid):
# 统计左半边的贡献
ret += query(pos * 2, l, mid, x, y)
if(y > mid):
# 统计右半边的贡献
ret += query(pos * 2 + 1, mid + 1, r, x, y)
return ret
线段树还有动态开点和标记永久化等写法,动态开点可以节约空间,标记永久化可以在结点很特殊的情况下(树套树)保证复杂度。

浙公网安备 33010602011771号