有 n 个位置,一开始全部是空的。每次操作把区间 [l, r] 整段覆盖掉,每次操作后输出还剩多少个空位。
下面用一棵 n = 16 的树来演示这份代码是怎么跑的 —— 点操作按钮,然后一步步看。
节点里存什么:x 表示这个区间里已经被覆盖的位置数,tg 表示“这个区间整段都满了”。
① 完全覆盖就停:如果当前区间整个落在操作区间里,直接 x = 区间长度、tg = true 返回,不再往下递归。这是第一层剪枝。
② 已经满了就跳过:往儿子递归之前,先看 tr[儿子].x 是不是已经等于儿子长度。是的话说明那一段早就满了,直接跳过。这是复杂度的关键 —— 一个节点一旦变满就永远不会再被深入访问,所以整棵树总共只会有 O(节点数) 次“真正往下走”的开销,每次操作额外只花 O(log n) 在两条边界路径上。
③ 动态开点:一开始只有根节点,只有真正要往下走时才 p = ++tot 给儿子分配编号。所以空间只跟“被访问过的节点数”有关,最多 O(q log n),而不是固定 4n。
④ 懒标记下推:如果带着 tg 的节点还要往下走,说明它的两个儿子其实也应该是满的(只是可能还没被创建出来)。下传时把两个儿子直接置满即可 —— 注意这份代码里只有“覆盖”没有“清空”,所以下推的动作就是把儿子填满。
⑤ 回溯合并:从儿子回来后 tr[p].x = tr[lc].x + tr[rc].x。没被创建过的儿子下标是 0,而 tr[0].x 恒为 0,所以代码不需要特判。