1. 前言
树上差分,是一种难度不高,思维量也不大的算法,应用范围比较窄但是快。
前置知识:差分,树上最近公共祖先(LCA)。
2. 详解
2.1 点上差分
先来看这么一个例题:
给出一棵 n n n 个点的树,每个点点权初始为 0,现在有 m m m 次修改,每次修改给出 x , y x,y x,y,将 x , y x,y x,y 简单路径上的所有点点权 + d +d +d,问修改完之后每个点的点权。
1 ≤ x , y ≤ n ≤ 1 0 6 , m ≤ 1 0 6 1 \leq x,y \leq n \leq 10^6,m \leq 10^6 1≤x,y≤n≤106,m≤106。
如果你会树链剖分,你能够直接秒了这道题,因为从树剖的角度看这就是个板子。
然而树剖解决这种问题的复杂度是 O ( n log 2 n ) O(n \log^2 n) O(nlog2n),虽然常数小并且跑不满,但是还是跑不过 1 0 6 10^6 106。
于是我们需要一种更加简洁的算法,就是树上差分。
首先简要回顾一下序列上的差分:
序列上的差分在处理区间加(设区间为 [ l , r ] [l,r] [l,r],加上的数为 d d d)的时候,采用的方法是在 a l a_l al 加上 d d d,在 a r + 1 a_{r+1} ar+1 减去 d d d,然后做一遍前缀和。
这样做的好处就是将区间加改成了单点加,简化了操作。
那么考虑将序列差分转移到树上:
比如我们要对 x , y x,y x,y 的路径上的点统一加上 d d d。
那么我们仿照序列差分的形式,首先 a y a_y ay 加上 d d d, a x a_x ax 加上 d d d。
发现我们影响的点是 x , h , b , f , y x,h,b,f,y x,h,b,f,y,因此对于点 a a a 我们不能有影响,操作方案就是 a b a_b ab 减去 d d d, a a a_a aa 减去 d d d。
最后我们对整棵树做一遍 dfs,将所有点的点权变为其子树(含自己)内所有点的点权,这个操作仿照求每个点子树的 Size 就可以完成了。
这么做的正确性是什么呢?
观察 a y + d , a x + d a_y+d,a_x+d ay+d,ax+d,发现这两个操作对于 a , b a,b a,b 及以上(这里没有)的点造成了影响, a a a 及以上的点是双倍影响( 2 × d 2 \times d 2×d), b b b 是单倍影响。
因此处理方案就是首先在 b b b 这里减去 d d d,然后 a a a 这里减去 d d d,这样 a a a 及以上的点影响就消除了, b b b 这个点也是正常的加上 d d d。
发现 b b b 是 l c a ( x , y ) lca(x,y) lca(x,y),于是对于一次修改操作就是这样的:
a x ← a x + d , a y ← a y + d , a l c a ( x , y ) ← a l c a ( x , y ) − d , a f a l c a ( x , y ) ← a f a l c a ( x , y ) − d a_x \leftarrow a_x+d,a_y \leftarrow a_y+d,a_{lca(x,y)} \leftarrow a_{lca(x,y)}-d,a_{fa_{lca(x,y)}} \leftarrow a_{fa_{lca(x,y)}}-d ax←ax+d,ay←ay+d,alca(x,y)←alca(x,y)−d,afalca(x,y)←afalca(x,y)−d。
这里需要注意的是代码中根节点 r o o t root root的父亲应该设为一个虚拟节点,否则 l c a ( x , y ) = r o o t lca(x,y)=root lca(x,y)=root 的时候会出问题。
上述问题就是点上差分,叫做点上差分的原因是因为这类问题是操作点权的。
2.2 边上差分
还是看这么一个例题:
给出一棵 n n n 个点的树,每条边边权初始为 0,现在有 m m m 次修改,每次修改给出 x , y x,y x,y,将 x , y x,y x,y 简单路径上的所有边边权 + d +d +d,问修改完之后每条边的边权。
1 ≤ x , y ≤ n ≤ 1 0 6 , m ≤ 1 0 6 1 \leq x,y \leq n \leq 10^6,m \leq 10^6 1≤x,y≤n≤106,m≤106。
会树剖的同学还是能秒了这道题,当然复杂度还是会炸。
接下来看看树上差分的优秀做法吧~
首先我们需要一种叫做“边权转点权”的方法,就是对于每个点我们认为其点权代表这个点与其父节点之间的边的边权,对于每条边我们认为其边权是这条边所连两个点中深度较大的点的点权,根节点点权无意义。
然后我们就可以开始利用树上差分了,还是修改 x , y x,y x,y 路径上的边,还是这张图:
发现此时改的点只有 x , y , h , f x,y,h,f x,y,h,f 了,于是我们这么操作: a x ← a x + d , a y ← a y + d , a l c a ( x , y ) ← a l c a ( x , y ) − 2 d a_x \leftarrow a_x+d,a_y \leftarrow a_y+d,a_{lca(x,y)} \leftarrow a_{lca(x,y)}-2d ax←ax+d,ay←ay+d,alca(x,y)←alca(x,y)−2d。
这么做的正确性可以仿照点上差分说明,这里不再赘述。
同样的做完之后一遍 dfs 求一下每个点的点权即可。
上述问题叫做边上差分,是因为该类问题在边上操作边权。
点上差分和边上差分其实区别不大,只是处理细节稍微有些不同而已。
2.3 例题
这道题是点上差分的例题,就是需要注意一下求完所有点真的点权之后对于所有 a i ( i ≥ 2 ) a_i(i \geq 2) ai(i≥2) 这些点而言,点权需要减一,因为这些点并不会被走两次。
Code:GitHub CodeBase-of-Plozia P3258 [JLOI2014]松鼠的新家.cpp
3. 总结
树上差分就是将路径加转变为了单点加,仿照差分思路解题而已。
实际上你会发现树上差分复杂度是 O ( n + m ) O(n+m) O(n+m) 的,并且码量相对较小,于是对于一些 n , m n,m n,m 比较大的题(树剖过不去)或者是树剖码量大的题,树上差分能够起到作用。