Codeforces 383C Propagating tree 从深度奇偶到子树差分
给一个顶点加上一个数,它的孩子减去这个数,孙子再加回来。直接模拟这条传播链并不困难,困难的是让二十万次操作都不再遍历整棵子树。
383C 的关键不是找到更复杂的树结构,而是先换一个数值坐标:奇数深度乘以 −1,偶数深度乘以 +1。原来交替正负的修改,在这个坐标里变成了整棵子树的同号修改。 接上 1006E 子树区间与树状数组计数,只需一棵差分树状数组。
子树是一段 DFS 序
理解进入时间与包含自身的闭区间。
先消除符号交替
更新端与查询端各乘一次深度符号。
用逐边传播对拍
独立模拟每经过一条父子边就变号。
官方操作与约束
题目为 Codeforces 383C Propagating tree。以下是操作概括,完整输入约定见官方页面;难度与标签也可在官方题库 API查询。
| 项目 | 官方信息 |
|---|---|
| 难度与标签 | 2000;data structures、dfs and similar、trees |
| 顶点 n 与操作 m | 均为 1 到 200000 |
| 初始值 a 与更新量 val | 均为 1 到 1000 |
| 根 | 顶点 1 |
1 x val |
x 加 val,向下每经过一条边,增量变号 |
2 x |
输出 x 当前的数值 |
| 时间与内存 | 2 秒;256 MB |
更新只向后代传播,包括 x 自身,不会传播到父节点或兄弟。初始值为正,不意味着答案始终为正;奇数距离的后代可能被减到零以下。所有操作按输入顺序执行,不能把查询挪到修改前后。
为什么逐点传播会超时
朴素方法从 x 开始遍历子树,沿每条边反转增量。一次根更新访问 n 个顶点,反复根更新最坏需要 O(nm) 时间。
DFS 序解决了“更新范围是什么”,却还没解决“区间里的每个位置应该加还是减”。深度奇偶在先序数组里不一定交替:先访问完一个孩子的整个分支,再进入下一个孩子,数组相邻位置可能同奇偶。因此不能按 DFS 序下标的奇偶性选择符号。
深度符号为什么能统一更新
设根深度为 0,定义 s(v):偶数深度取 +1,奇数深度取 −1。对 x 的后代 v,路径长度是 depth[v] − depth[x],本次增量可以写成:
1 | 真实增量 Δ(v) = s(v) × s(x) × val |
最后一步用了 s(v) × s(v) = 1。对整个 x 子树,s(x) × val 是同一个数,与 v 的深度无关。
于是更新只做一件事:把统一增量 s(x) × val 加到子树区间。查询则恢复原坐标:
1 | 当前值(v) = a[v] + s(v) × b(v) |
初始数组不必放进树状数组,因为 b 只累计后续修改。这样树状数组初始全零,查询单独加回 a[v]。也可以维护符号变换后的完整数值,但不能把两种初始化方式混在一起。
flowchart LR A["更新 x 加 val"] --> B["后代 v 增量为 s(v) s(x) val"] B --> C["乘以目标符号 s(v)"] C --> D["整棵子树统一增加 s(x) val"] D --> E["查询时再乘 s(v),加回初始值"]
手工演算更新端与查询端
用一棵六顶点的小树:根 1 的孩子为 2、3;2 的孩子为 4、5;3 的孩子为 6。按这个孩子顺序做 DFS,先序为 [1,2,4,5,3,6]。
| 顶点 | 深度 | s(v) | tin | tout | 初始值 |
|---|---|---|---|---|---|
| 1 | 0 | +1 | 1 | 6 | 10 |
| 2 | 1 | −1 | 2 | 4 | 20 |
| 3 | 1 | −1 | 5 | 6 | 30 |
| 4 | 2 | +1 | 3 | 3 | 40 |
| 5 | 2 | +1 | 4 | 4 | 50 |
| 6 | 2 | +1 | 6 | 6 | 60 |
执行 1 2 7 时,变换后的区间 [2,4] 统一加 −7。查询顶点 2 得到 20 + (−1)×(−7) = 27;查询顶点 4 得到 40 + (+1)×(−7) = 33。同一棵树状数组给出相同的累计量,查询端的符号负责恢复不同的真实增量。
再执行根更新与叶子更新,逐点结果如下。每一列始终按顶点编号排列,而不是 DFS 顺序。
| 已执行的操作 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| 初始 | 10 | 20 | 30 | 40 | 50 | 60 |
1 2 7 |
10 | 27 | 30 | 33 | 43 | 60 |
再执行 1 1 3 |
13 | 24 | 27 | 36 | 46 | 63 |
再执行 1 5 4 |
13 | 24 | 27 | 36 | 50 | 63 |
第三次更新的 x 深度为偶数,因此变换坐标里加 +4。观察顶点 6:它不属于子树 2,也不属于子树 5,只有根更新影响它。范围与符号必须分别正确。
DFS 闭区间与差分树状数组
进入顶点 v 时记 tin[v]=++timer;它的全部后代处理完后,记 tout[v]=timer。进入时间不重复计数,退出时也不新增序号。于是子树恰好对应闭区间 [tin[v],tout[v]],叶子满足 tin=tout。
迭代 DFS 用“进入事件”和“退出事件”代替递归调用栈:先放入退出事件,再倒序放入孩子的进入事件。栈后进先出,确保孩子全部处理完才退出当前顶点。倒序只是为了让孩子按邻接表正序访问;本题并不要求固定孩子顺序。
| 数组或对象 | 含义 |
|---|---|
parent[v] |
根为 0 的父节点,用来跳过无向边的反向访问 |
sign[v] |
根取 +1,孩子取父节点符号的相反数 |
tin[v]、tout[v] |
从 1 开始的子树闭区间 |
a[v] |
原始数值,始终不修改 |
| Fenwick | 变换后累计增量 b 的差分数组 |
对 b 的数组区间 [l,r] 加 d,只需在差分数组的 l 加 d、r+1 减 d。对位置 p 求差分前缀和,正好得到 b[p]。
本题不是区间和查询,所以一棵树状数组就够了;不需要两棵 BIT 的加权前缀公式,也不需要维护区间总和的线段树。代码仅存下标 1 到 n;r=n 时不写 n+1,它不会参与任何合法位置的前缀查询。
flowchart TD A["进入 v:记录 tin"] --> B["把退出事件先放到栈里"] B --> C["依次完成全部孩子的进入与退出"] C --> D["退出 v:记录当前 timer 为 tout"] D --> E["修改 tin 加 d;若 tout 小于 n,在 tout+1 减 d"] E --> F["查询 tin 前缀和,再恢复真实值"]
正确性证明
子树连续性。 DFS 进入 v 后,只有完成全部孩子的子树才执行 v 的退出事件。期间获得进入编号的顶点恰好是 v 及其后代,因此 [tin[v],tout[v]] 不包含子树外顶点,也不漏掉任何子树内顶点。
单次更新等价性。 对后代 u,真实增量是 s(u)s(v)val,乘以 s(u) 后变成统一的 s(v)val。对子树外顶点两种表示都不修改。故一次区间加与一次题目传播操作,在变换坐标里完全等价。
差分还原性。 一次更新在 l 留下 +d,在 r+1 留下 −d。p<l 时前缀未包含任何端点;l≤p≤r 时只包含 +d;p>r 时两项抵消。多次更新按加法叠加,因此前缀查询得到所有已执行更新的 b 值。
初始 b 为零,与初始数值一致。上述等价性保证每次修改保持这一关系;查询用 a[u]+s(u)b(u) 逆变换,因 s(u) 的平方为 1,恢复值正是题目要求的当前数值。归纳可知全部操作正确。
边界与错误清单
| 容易写错的地方 | 正确做法与反例 |
|---|---|
| 用 tin 的奇偶代替深度奇偶 | 例中顶点3的 tin=5,却与 tin=2 的顶点2同为奇数深度 |
| 更新乘了 s(x),查询忘记乘 s(v) | 1 2 7 后顶点2不是 20−7,而是 20+7 |
| 初始值重复计入 BIT 与 a | 本实现只在 BIT 里保存修改增量 |
| 写成半开区间,却仍在 tout+1 抵消 | 统一使用包含自身的闭区间 |
| 叶子没有退出事件 | 叶子的 tout 必须等于自己的 tin |
| 根更新遗漏最后一个位置 | r=n 时跳过右端抵消,不是跳过整次修改 |
| 按编号猜测父子关系 | 输入是无向树,父节点由根为1的遍历决定 |
| 默认所有答案为正 | 减法传播允许合法的负答案 |
| 对二十万点链递归 DFS | 显式事件栈,空间由容器承担 |
官方范围内,单点累计增量绝对值不超过 m×1000,32 位有符号整数可以覆盖当前数值范围。实现仍选 long long,让数值类型保持明确,并为复用到更大更新量的练习留余地;不能据此把官方 val 上限说成 10^9。
完整 C++17 实现
1 |
|
复杂度与适用范围
读边与迭代 DFS 都为 O(n),每次修改至多两次 BIT 更新,查询一次前缀和。因此总时间 O(n+m log n),空间 O(n)。显式事件栈在星形或长链树上最多 O(n) 个待处理事件。
| 变化后的问题 | 这套方法是否直接适用 |
|---|---|
| 固定树上的子树更新与单点查询 | 可以,正是当前模型 |
| 任意两点路径上的隔层修改 | 不能只用一个子树区间,需要额外路径分解 |
| 更新后查询整棵子树的真实值总和 | 当前单棵差分 BIT 不够,需要维护相应的带符号区间统计 |
| 在线换根或改变边 | 当前 tin、tout 与固定深度符号不能直接照搬 |
| 每条边乘的因子不再是 −1 | 重新检查因子是否能分离为更新端与查询端两部分 |
继续比较 620E 子树染色与 343D Water Tree:它们改变的是颜色或覆盖状态,不能把不同的标记合成方式混成“都是懒标记”。383C 先通过代数变换统一增量,再用差分解决当前需要的查询;数据结构是在状态定义之后选择的。