线段树
线段树当中的几个重要操作
1.PushUp
上推操作:由子节点算父节点的信息
\(push up\) 操作的目的是为了维护父子节点之间的逻辑关系。当我们递归建树时,对于每一个节点我们都需要遍历一遍,并且电脑中的递归实际意义是先向底层递归,然后从底层向上回溯,所以开始递归之后必然是先去整合子节点的信息,再向它们的祖先回溯整合之后的信息。
我们在这儿就能看出来,实际上push_up是在合并两个子节点的信息,所以需要信息满足结合律!
2.PushDown
下沉操作,又称为懒标记或延迟标记:由父节点计算子节点的信息
之所以称为懒标记,是因为当我们在修改一个区间的信息的时候,我们不会把这个区间和它的所有子区间全都修改,而是在根节点的区间上标记一下(这就需要一个额外的信息 \(add\) 来储存我们的修改信息),在用到该区间的时候才进行修改。即延迟修改。
例如我们要修改区间 \([1, 10]\),我们无须把它的子区间 \([1, 5]\) 和 \([6, 10]\) 以及 \([1, 5]\) 和 \([6, 10]\) 的子区间全部递归修改一番直到修改到叶子节点然后逐层回溯,这样操作的时间复杂度是可以高达 \(O(NlogN)\) 的。
我们可以直接在区间 \([1, 10]\) 标记一下,表示我们修改了这个区间了,但是我们不对他的子区间进行操作,直到某一次查询或者修改需要使用这个区间的时候,我们才修改这个区间并把它的修改下沉到子区间。
不过我的习惯是,当我们需要标记一个区间的时候,直接就把此区间修改了。
总而言之,懒标记的作用是记录每次、每个节点要更新的值,也就是delta,但线段树的优点不在于全记录(全记录依然很慢qwq),而在于传递式记录:
整个区间都被操作,记录在公共祖先节点上;只修改了一部分,那么就记录在这部分的公共祖先上;
3.Build
将一段区间初始化为线段树
4.Modify
- 单点(\(PushUp\))
- 区间(\(PushDown\) + \(Pushup\))
5.Query
时间复杂度:\(O(4 \times logN)\)
查询某一段区间的信息
6.与树状数组的比较
线段树的常数比树状数组
线段树的 \(logn\) 是 \(4\) 倍的 \(logn\)
树状数组的 \(logn\) 是 \(1\) 倍的 \(logn\)
线段树点修改和区间查询的时间复杂度为
7.\(up\)和\(down\)操作的使用时机
主要是从定义出发
(1)\(pushup\)
\(pushup\) 的定义是通过合并子节点的信息并上传修改根节点的信息,只要修改子节点的信息,就要在回溯的过程中修改父节点的信息。
因此,\(pushup\) 一般是在代码的末尾出现,只有子节点都修改完成之后,才可以吧子节点的信息合并推到父节点上。
例如:
(1)建树的过程中,如果区间可以继续划分,那么我们就要递归该根节点的左右区间,当递归左右区间结束的时候,就要 \(pushup\) 合并返回子节点的信息。
(2)修改区间信息,如果我们修改的区间不能在当前区间直接完成修改,而是要在它的左右区间分别修改一部分,例如区间 \([1, 10]\),我们要修改 \([4, 7]\) ,就需要分别修改它的左子树区间 \([1, 5]\) 和右子树区间 \([6, 10]\)。并在修改结束的时候 \(pushup\) 合并子节点的修改信息返回到父节点。
(2)\(pushdown\)
\(pushdown\)的定义是下沉父节点的修改信息到子节点,只要父节点的信息被修改,就要下沉父节点的修改信息到子节点上。
因此, \(pushdown\) 一般在代码的头出现,否则此时子节点的信息未被修改,或者说出现修改重叠的情况。
并且,使用 \(pushdown\) 的时候一般都是在区间分裂的时候使用的。即,我们不能在当前区间直接完成操作,需要分别在它的左区间或者右区间或者两个子区间进行操作。而由于我们使用了懒标记,只修改了根区间的信息,它的子区间还未修改,所以说我们要把它的修改信息下沉到子区间并修改和标记子区间。
例如:
(1)执行查询操作,此时我们得到区间的一些信息,当然要把还未执行的操作执行一下。
(2)执行修改操作,这个不太好理解,虽然说我们的修改操作也可能会使用到两个子区间的信息,但我们只是对区间加上一个懒标记,为什么还要把根区间的懒标记下沉呢?这是因为我们上面的 pushup 会出现在修改操作里,如果懒标记没有下传就会导致子节点的值没有第一时间更新,父节点就会被错误的更新。
再举个例子解释一下第(2)点:
- 初始化区间 \([1, 10]\) 所有元素全为 \(1\),建树,维护两个信息:\(lazyTag\) 和 区间和 \(sum\)
- 执行修改操作 \([1, 10]\) 的每个元素都加上 \(2\),此时区间元素全为3。我们的懒标记修改了区间 \([1, 10]\) 的 \(sum\),\(sum[1, 10] = 30\),但此时区间 \([1, 5], [6, 10], ... ,[9, 9],[10, 10]\)的值依然是初始状态下的值。 最后 \(pushup\) (由于 \([1, 10]\) 为整个树的根节点,所以此时 \(pushup\) 啥都没做)。
- 再次执行修改操纵 \([1, 5]\) 的每个元素都加上 \(1\)(不执行 \(pushdown\)),紧接着由于 \([1, 5]\) 无法包含当前区间 \([1, 10]\) ,分裂区间为 \([1, 5], [6, 10]\), 递归到左区间 \([1, 5]\),此时正好包含该区间,修改区间 \([1, 5]\) 的值为 \(10\),然后 \(pushup\) 该区间的修改信息到父区间 \([1, 10]\), \(sum[1, 10] = sum[1, 5] + sum[6, 10] = 10 + 5 = 15\),然后回溯到父区间 \([1, 10]\),执行 \(pushup\)(由于是树根不执行),结束。
- 最后我们发现第二次操作 \(sum[1, 10] = 15\),甚至比第一次操作的 \(sum\) 更小了,究其原因在于第一次操作给区间 \([1, 10]\) 打上的懒标记没有在第二次操作下沉到子区间,导致子区间实际上没有执行第一次修改操作。
- 如果我们在分裂区间之前执行 \(pushdown\),那么此时有 \(sum[1, 5] = 15, sum[6, 10] = 15\),然后执行第二次修改操作 \(sum[1, 5] = 15 + 5 = 20\),最后合并区间 \([1, 5], [6, 10]\) 的和上传到区间 \([1, 10]\),此时 \(sum[1, 10] = 20 + 15 = 35\) 就正确了
8.常见的SE错误来源
- 数组空间没有开四倍
- \(mid = l + r >> 1\) 还是 \(mid = tr[u].l + tr[u].r >> 1\)
- \(u << 1\) 或者 \(u << 1|1\) 是否写错
- \(build\) 里面 \(else\) 循环中,是否初始化 \(tr[u] =\{l, r... \}\)