差分

1 差分

设原数组的第ii个元素为aia_i,那么差分数组的第ii个元素为di=ai−ai−1d_i = a_i- a_{i-1}

有一组数据如下

下标123456原数组 a[i]351784差分数组 d[i]32−461−4差分数组d前缀和 s[i]351784\begin{array}{r|c|c|c|c|c|c} \text{下标} & 1 & 2 & 3 & 4 & 5 & 6 \\ \hline \text{原数组 } a[i]& 3 & 5 & 1 & 7 & 8 & 4 \\ \text{差分数组 } d[i]& 3 & 2 & -4 & 6 & 1 & -4 \\ \text{差分数组$d$前缀和 } s[i]& 3 & 5 & 1 & 7 & 8 & 4 \\ \end{array}

发现:对差分数组进行前缀和操作,就可以得到原数组了

Si=∑j=1idi=d1+d2+⋯+di=(a1−a0)+(a2−a1)+⋯+(ai−ai−1)=(a1−a0)+(a2−a1)+⋯+(ai−ai−1)=ai−a0      因为a0=0=ai\begin{aligned} S_i = \sum_{j=1}^i d_i &= d_1 +d_2 + \cdots + d_i \\ &= (a_1 - a_0) + (a_2 - a_1) + \cdots + (a_i - a_{i-1}) \\ &= (\bcancel{ a_1 } - a_0) + (\bcancel{a_2} - \bcancel{a_1}) + \cdots + (a_i - \bcancel{a_{i-1}}) \\ & = a_i - a_0 \;\;\; \text{因为$a_0 = 0$} \\ & = a_i \end{aligned}

现在对原数组aa的区间[2,4][2,4]上的每个数都增加22,得到如下

下标123456原数组 a[i]373984差分数组 d[i]34−46−1−4差分数组d前缀和 s[i]373984\begin{array}{r|c|c|c|c|c|c} \text{下标} & 1 & \color{Blue}{2} & \color{Blue}{3} & \color{Blue}{4} & 5 & 6 \\ \hline \text{原数组 } a[i]& 3 & \color{Blue}{7} & \color{Blue}{3} & \color{Blue}{9} & 8 & 4 \\ \text{差分数组 } d[i]& 3 & \color{Blue}{4} & -4 & 6 & \color{Blue}{-1} & -4 \\ \text{差分数组$d$前缀和 } s[i]& 3 & \color{Blue}{7} & \color{Blue}{3} & \color{Blue}{9} & 8 & 4 \\ \end{array}

发现,对原数组进行区间[2,4][2,4]修改: 差分数组只会修改两个值,第一个值为b2=b2+2b_2 = b_2+2,第二个值为b5=b5−2b_5 =b_5-2,且对修改后的差分数组进行前缀后依然得到了原数组.

于是,得出结论,如果对原 数组进行区间[l,r][l,r]上的每个数都增加vv,那么对应的差分数组只需要修改两位置bl=bl+vb_l = b_l+v, br+1=br+1−vb_{r+1} = b_{r+1}-v.原来需要r−l+1r-l+1次的区间操作,现在只需要进行22次区间操作.也就是把区间增减转了两次的单点增减.大大节省了时间

对于,那些题目

  • 多次区间修改(增减)
  • 一次查询(查询原数组的值)

适合使用差分思想(把原数组转化成差分数组来操作)

2 思考与总结

差分是前缀和的逆运算

已知,原数组aa就是数组ss,两者一模一样.所以在这里我们其实是先假定数组aa是数组dd形成的前缀和数组,进行就想到能否通过前缀和数组aa推导出数组dd呢?

我们现在认为原数组aa上每个值aia_i都是某个数组dd的前缀和.那么可以得到

ai=ai−1+di(1) a_i = a_{i-1} + d_i \tag 1

移项,进而得到

di=ai−ai−1(2) d_i = a_i - a_{i-1} \tag 2

通过(2)(2)式,把数组aa转化成dd数组.也就是前缀和数组转化成差分数组

可以通过aa得到dd,也可以通过dd得到aa,他们互为逆运算

a⇌d a \rightleftharpoons d

可以认为,aa与dd是同一事物的不同表现形式.

综上,可以得到如下的性质:

设dd是原数组,aa是dd的前缀和,

  • 在dd的两个位置i,ji,j上,同时+v,−v+v,-v,也就是di=di+v,dj=dj−vd_i = d_i+v,d_j = d_j-v,那么体现到aa上,那就是区间[i,j−1][i,j-1]上的每个数都增加vv
  • 在aa上的区间[i,j−1][i,j-1]上的每个数都增加vv,那么体现到dd上,那就是位置i,ji,j,+v,−v+v,-v,也就是di=di+v,dj=dj−vd_i = d_i+v,d_j = d_j-v

3 函数思想

设aa是一个数字组成的序列,定义函数如下:

  • f(a)=bf(a) = b,把序列aa转化成bb,其中bi=∑1iaib_i = \sum_1^i a_i,也就是前缀和
  • f−1(a)=bf^{-1}(a) = b,表示把序列aa转化成bb,其中bi=ai−ai−1b_i = a_i - a_{i-1},也就是差分

显然ff与f−1f^{-1}互为逆运算,也就是

f−1(f(a))=af(f−1(a))=a \begin{gather} f^{-1}(f(a)) = a \\ f(f^{-1}(a)) = a \end{gather}

同样可以想到ff与f−1f^{-1}是双射函数,也就是f(a)=bf(a) = b 也就是说ranfranf与domfdomf一一映射

定义函数range(a,l,r)range(a,l,r),表示对序列aa区间[l,r][l,r]上的每个数都增加11

range(a1,l,r)=a2 range(a_1,l,r) = a_2

表示a1a_1经过区间[l,r][l,r]上的 每个数都增加11后得到a2a_2,显然a1≠a2a_1 \neq a_2,则f−1(a1)=d1≠f−1(a2)=d2f^{-1}(a_1) = d_1 \neq f^{-1}(a_2) = d_2^[因为一一映射的性质]

根据差分的性质知d1,d2d_1,d_2只有两个点不同,且这两个点是l,r+1l,r+1

定义函数

定义函数add(a,l,r)add(a,l,r),表示对序列aa上的两点l,r+1l,r+1分别+1,−1+1,-1

add(a1,l,r)=a2 add(a_1,l,r) = a_2

则range(a1,l,r)range(a_1,l,r)与add(d1,l,r)add(d_1,l,r)是一一映射的.也就是每一个在序列a+1a+1上的rangerange操作,都等价于在序列d1=f−1(a1)d_1 = f^{-1}(a_1)上的add操作^[这里其时使用了反证法]

4 二维差分

11−9−3511−9−3511−9−35 \def\arraystretch{2} \begin{array}{|c : c : c:c|} \hline 11 &-9& -3& 5\\ \hline 11 &-9& -3& 5\\ \hline 11 &-9& -3& 5\\ \hline \end{array}

根据上面的思考,可以认为此时的二维矩阵上的值,都是每个二维矩阵dd的前缀和.也就是

⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡⊡ \begin{array}{cccccc} \color{Blue}{\boxdot} & \color{Blue}{\boxdot} &\color{Blue}{\boxdot} & \color{Green}\boxdot & \boxdot \\ \color{Blue}{\boxdot} & \color{Blue}{\boxdot} &\color{Blue}{\boxdot} & \color{Green}\boxdot & \boxdot \\ \color{Blue}{\boxdot} & \color{Blue}{\boxdot} &\color{Blue}{\boxdot} & \color{Green}\boxdot & \boxdot \\ \color{Green}{\boxdot} & \color{Green}{\boxdot} &\color{Green}{\boxdot} & \color{Red}{\boxdot} & \boxdot \\ \boxdot & \boxdot & \boxdot & \boxdot & \boxdot & \end{array}
Si,j=Si−1,j+Si,j−1−Si−1,j−1+di,j S_{i,j} = S_{i-1,j} + S_{i,j-1} - S_{i-1,j-1} + d_{i,j}

移项,得到原二维矩阵上的每个值为:

di,j=Si,j−Si−1,j−Si,j−1+Si−1,j−1 d_{i,j} = S_{i,j}-S_{i-1,j}-S_{i,j-1} + S_{i-1,j-1}

对SS矩阵上的子矩阵(xi,yi),(xj,yj)(x_i,y_i),(x_j,y_j)进行+v+v操作,反应到dd上就是

  • (xi,yi),(xj+1,yj+1)(x_i,y_i),(x_{j+1},y_{j+1})上的每个数都+v+v
  • (xi,yj+1),(xj+1,yi)(x_{i},y_{j+1}),(x_{j+1},y_{i})上的每个数都−v-v

小技巧: 点di,jd_{i,j}的值是Si,j−Si−1,j−Si,j−1+Si−1,j−1S_{i,j} - S_{i-1,j} - S_{i,j-1} + S_{i-1,j-1}, 也就是说,与以i,ji,j作为左下角的四个格子的值有关.也就是说i,ji,j的覆盖范围,有奇数个+v+v时才会产生影响.

martix

题目: luogu P3397: 地毯

5 差分与贡献

5.1 线性序列上的贡献模型

引入

有 44 个数字 a1,a2,a3,a4a_1,a_2,a_3,a_4,初始值依次为 1,2,3,41,2,3,4。每个数字 aia_i 对后面的所有位置 [i+1,n][i+1,n],各贡献一次自己的初始值。

例如,a1a_1 使位置 2,3,42,3,4 的值各增加 11。每个位置还保留自己的初始值。

下标 a1a_1 a2a_2 a3a_3 a4a_4
初始值 11 22 33 44

所有贡献结算完毕后,每个位置的最终值是多少?

输入:

4
1 2 3 4

输出:

1 3 6 10

用 aa 保存不变的初始值,用 bb 保存最终值。直接做法是先令 bi=aib_i=a_i,再枚举每个贡献者及其影响的位置。

暴力代码(核心片段)

点击
//自己对自己的贡献
for (int i = 1; i <= n; ++i) b[i] = a[i];
for (int i = 1; i <= n; ++i) {
    for (int j = i + 1; j <= n; ++j) {
        b[j] += a[i];
    }
}

时间复杂度为 O(n2)O(n^2)。慢在每个 aia_i 都要逐个访问它后面的所有位置,而这些后缀大量重叠。

5.2 两种视角:推土机与接收者

每个数都要给后面所有位置贡献,能不能只交给紧挨着的下一个?

先试着让位置 11 只给位置 22 贡献,执行 a[2] += a[1]。位置 22 从 22 变成 2+1=32+1=3,但位置 3,43,4 还没有收到位置 11 的贡献。

位置 11 还没完成的贡献,由谁继续送?

由位置 22 接着送。注意,此时的 a[2] 已经不是初始值 22,而是 2+12+1:既有位置 22 自己的贡献,也有位置 11 交过来的贡献。

因此,下一步执行 a[3] += a[2],加过去的就是整包 2+12+1,位置 33 变成 3+(2+1)=63+(2+1)=6。这一步同时完成了两件事:位置 22 给位置 33 贡献了 22,还替位置 11 把那份 11 继续传给了位置 33。

那么,位置 44 怎么收到前三个位置的贡献?

继续做同一个动作:a[4] += a[3]。此时 a[3] 已经装着 3+2+13+2+1,所以位置 44 一次就收到了前三个位置的贡献,变成 4+(3+2+1)=104+(3+2+1)=10。

把这三步连起来看。表中的数字始终表示各个位置的初始值来源,括号表示这一步传过去的整包贡献:

操作 a1a_1 a2a_2 a3a_3 a4a_4
初始 11 22 33 44
a[2] += a[1] 11 2+(1)=32+(1)=3 33 44
a[3] += a[2] 11 2+1=32+1=3 3+(2+1)=63+(2+1)=6 44
a[4] += a[3] 11 2+1=32+1=3 3+2+1=63+2+1=6 4+(3+2+1)=104+(3+2+1)=10

交互动画加载中...

这就是这里的“推土机”:每个位置把收到的贡献,连同自己的初始值,一起传给下一个位置。“传给”表示给下一位置加上一份,当前位置的值仍然保留。

于是,不用让每个数亲自跑遍后面所有位置,只需从左向右执行:

for (int i = 1; i < n; ++i) {
    // a[i] 已包含前面所有位置的贡献,以及自己的初始值
    // 把这一整包贡献传给下一个位置
    a[i + 1] += a[i];
}

这里直接修改 aa,循环结束后,aa 中保存的就是最终结果;最后一个位置没有下一个位置,所以循环条件是 i < n。

为什么这样传递不会漏算或重算?

执行到位置 ii 时,前面各位置的初始值都已沿相邻位置传到这里,且各有一份。下一步只向位置 i+1i+1 传递一次,再加上位置 i+1i+1 原有的初始值,就又得到了各有一份的新一包贡献。如此继续,每个初始值都恰好经过它后面的每个位置一次。

整个过程只有 n−1n-1 次相邻传递,时间复杂度为 O(n)O(n)。从关系上看,“给后面所有位置贡献”被拆成了相邻位置之间的接力;这里还依赖从前到后只有一条传递路径。后面推广到 DAG 时,要特别留意路径不唯一的情况。

站在接收者的位置看,答案又是什么?

为了写清公式,下面重新用 aa 表示不变的初始值,用 bb 表示最终值。位置 ii 接收所有 j<ij<i 的贡献,再保留自己的初始值,因此

bi=ai+∑j=1i−1aj=∑j=1iaj. \begin{aligned} b_i &= a_i+\sum_{j=1}^{i-1}a_j \\ &=\sum_{j=1}^{i}a_j. \end{aligned}

这就是前缀和。这里 aia_i 是位置 ii 自己保留的值,并不是题目额外规定的一次自我贡献。

从贡献者出发,把累计值送给下一位置,是“刷表 / Push”的视角;从接收者出发,写成 bi=bi−1+aib_i=b_{i-1}+a_i(b0=0b_0=0),是“填表 / Pull”的视角。两者描述的是同一个计算过程。

注意

贡献取的是初始值。若直接对每个后方位置执行 b[j] += b[i],就会把已经收到的贡献再次发出,导致重复计算。

可以令 b = a 后仅执行相邻转移 b[i + 1] += b[i]:此时 b[i] 表示需要向下一位置传递的整包贡献,而不再是单个贡献者的初始值。

5.3 从累计贡献理解差分

前缀和负责向后传递,那差分数组里的一个数代表什么?

把差分数组 dd 当作贡献源。还原时执行

a0=0,ai=ai−1+di, a_0=0,\qquad a_i=a_{i-1}+d_i,

意味着 did_i 从位置 ii 开始,对后面每个位置都贡献一次。因此,d[l] += v 会使整个后缀 [l,n][l,n] 增加 vv。

如果只想影响 [l,r][l,r],怎样让贡献停止?

在 r+1r+1 放一个抵消源:d[r + 1] -= v。两个贡献源叠加后,各位置得到的增量为

Δai={0,i<l,v,l≤i≤r,v−v=0,i>r. \Delta a_i= \begin{cases} 0, & i<l, \\ v, & l\le i\le r, \\ v-v=0, & i>r. \end{cases}

于是,差分标记负责说明贡献从哪里开始、在哪里抵消,前缀和负责把这些标记沿相邻位置累计展开。这就是差分能够把一次区间修改变成两次单点修改的原因。

从逆运算看,固定 a0=0a_0=0 后,

di=ai−ai−1⟺ai=∑j=1idj. d_i=a_i-a_{i-1} \quad\Longleftrightarrow\quad a_i=\sum_{j=1}^{i}d_j.

这是一一对应的两种表示;加法也能保留:原数组的增量之和,对应差分标记的增量之和。因此可以先叠加所有修改的标记,最后统一还原。若使用“同构”这个词,这里指的是保持加法结构的对应,而不只是形式相似。

5.4 从单链推广到树

现在把一条链换成一棵以 11 为根的树。规定每个节点的初始权值贡献给它的所有祖先,每个节点也保留自己的初始权值。

      1
     / \
    2   3
   / \   \
  4   5   6

图中的边表示父子关系,贡献沿孩子到父亲的方向传递。例如 4,54,5 的贡献先到 22,再到 11;66 的贡献先到 33,再到 11。方向由关系决定,与节点编号的大小无关。

父节点能否只接收孩子打包后的贡献?

可以。设 sus_u 表示节点 uu 及其所有后代的初始权值之和。节点 uu 的各棵孩子子树互不相交,因此

su=au+∑v∈children⁡(u)sv. s_u=a_u+\sum_{v\in\operatorname{children}(u)}s_v.

例如 s2=a2+s4+s5s_2=a_2+s_4+s_5。只需先算出所有孩子的 svs_v,再算 sus_u,最后将 sus_u 传给父亲。这就是后序遍历 / 自底向上汇总。

每条父子边只传递一次,整棵树的汇总时间为 O(n)O(n)。任意后代到祖先的路径唯一,所以每份初始权值在对应祖先处恰好出现一次。

所以可以使用的方法是

  1. 后序遍历
vector<int> g[maxn];   // g[u] = u 的孩子,贡献方向 孩子 -> 父亲
long long a[maxn];     // 初始权值
long long s[maxn];     // s[u] = u 及其所有后代的权值和

// 后序遍历:先算完所有孩子,再把它们打包好的贡献收上来
void dfs(int u) {
    s[u] = a[u];                 // 先放自己的初始权值
    for (int v : g[u]) {
        dfs(v);
        s[u] += s[v];            // 孩子子树和已经算好,直接收上来
    }
}

2 .bfs + 倒序队列

vector<int> g[maxn]; // g[u] = u 的孩子,贡献方向 孩子 -> 父亲
long long a[maxn];   // 初始权值
long long s[maxn];   // s[u] = u 及其所有后代的权值和
vector<int> order;   // BFS 序:父亲一定排在孩子前面

// BFS + 倒序队列:倒着扫 BFS 序,等价于自底向上汇总
// --- bfs begin
queue<int> q;
q.push(1); // 根入队
while (!q.empty()) {
    int u = q.front();
    q.pop();
    order.push_back(u);           // 出队时记录顺序
    for (int v : g[u]) q.push(v); // 孩子入队
}
// --- bfs end

for (int u = 1; u <= n; ++u) s[u] = a[u]; // 先放自己的初始权值
reverse(order.begin().order.end());
for (auto u : order) { // 倒序遍历 BFS 序
    for (int v : g[u])
        s[u] += s[v]; // 此时 s[v] 已是完整子树和
}

5.5 多链 DAG:能到达,不代表只到达一次

如果一个节点可以把贡献传向多个后继,就得到一般的 DAG。仍然把边 v→uv\to u 定义为“贡献由 vv 传给 uu”。

怎样保证向外传递时,当前节点已经收齐贡献?

按这个方向的拓扑序处理:先令 su=aus_u=a_u,再对每条边 v→uv\to u 执行 s[u] += s[v]。拓扑序保证所有前驱都在 uu 之前处理。对于前面的树,孩子先于父亲的后序遍历就是一种合法顺序。

点击

拓扑序 + 沿边累加(每条路径各产生一份贡献)

vector<int> g[maxn]; // g[v] = v 的后继,贡献沿 v -> u 传递
int indeg[maxn];     // indeg[u] = u 还有几个前驱的贡献没收到
long long a[maxn];   // 初始权值
long long s[maxn];   // s[u] = 沿所有路径收到的贡献之和

// 拓扑序:保证处理 u 时,所有能到达 u 的前驱都已处理完
void dag_topo() {
    for (int u = 1; u <= n; ++u) s[u] = a[u]; // 先放自己的初始权值

    queue<int> q;
    for (int u = 1; u <= n; ++u)
        if (indeg[u] == 0) q.push(u); // 没有前驱的先入队

    while (!q.empty()) {
        int v = q.front();
        q.pop();
        for (int u : g[v]) {
            s[u] += s[v]; // v 的整包贡献交给 u
            if (--indeg[u] == 0) q.push(u); // u 已收齐,可以继续往后传
        }
    }
}

DAG 的可达关系加上自身关系后构成偏序;拓扑序给出了一个与该偏序相容的处理顺序。但是,处理顺序正确并不能消除多条路径带来的重复。

考虑菱形结构,所有箭头都沿贡献方向:

菱形 DAG:4 分别指向 2、3,再汇合到 1

令 a4=1a_4=1,其他节点初始值为 00。节点 2,32,3 都收到 11,最终节点 11 收到 22,因为从 44 到 11 有两条路径。

因此,上面的累加实际计算的是

su=∑vav P(v,u), s_u=\sum_v a_v\,P(v,u),

其中 P(v,u)P(v,u) 是从 vv 到 uu 的有向路径条数,约定 P(u,u)=1P(u,u)=1,不可达时为 00。

  • 若每条路径都应产生一份贡献,按拓扑序累加正确,时间为 O(n+m)O(n+m),其中 mm 是边数。
  • 若每个可达节点只应贡献一次,就必须去重。例如维护包含自身的可达来源集合,沿边做集合并;规模允许时可用 bitset,最后对集合中的节点权值求和。其代价取决于集合表示,不能再直接套用上述 O(n+m)O(n+m) 的求和结论。
点击

可达集合去重(每个可达节点只算一次)

vector<int> g[maxn];              // g[v] = v 的后继
int indeg[maxn];
long long a[maxn];
long long s[maxn];                // s[u] = 能到达 u 的来源(含自身)的权值和
bitset<maxn> reach[maxn];         // reach[u] = 能到达 u 的节点集合(含 u 自己)

// 每个可达节点只贡献一次:沿边做集合并,再对集合内的权值求和
void dag_reach_dedup() {
    vector<int> order;            // 拓扑序
    queue<int> q;
    for (int u = 1; u <= n; ++u) {
        reach[u].reset();
        reach[u].set(u);          // 自己一定能到达自己
        if (indeg[u] == 0) q.push(u);
    }

    while (!q.empty()) {
        int v = q.front();
        q.pop();
        order.push_back(v);
        for (int u : g[v]) {
            reach[u] |= reach[v]; // 集合并:v 能到达的,u 也能到达
            if (--indeg[u] == 0) q.push(u);
        }
    }

    for (int u = 1; u <= n; ++u) {
        s[u] = 0;
        for (int v = 1; v <= n; ++v)
            if (reach[u].test(v)) s[u] += a[v];
    }
}

在上面那张菱形图上取 a4=1a_4=1、其余为 00:拓扑累加版在节点 11 得到 22(两条路径各算一次),去重版得到 11。

从单链推广到多链时,除了检查依赖顺序,还要检查多条路径是否重复携带了同一份贡献。树上的 LCA 可以帮助处理树路径的交汇,但不能直接用来解决一般 DAG 的可达集合去重。

5.6 进阶:树上差分

树上已经知道怎样累计,能否反过来设计贡献源?

可以。设 dud_u 是节点 uu 的标记,令自底向上汇总后的结果为

cu=du+∑v∈children⁡(u)cv. c_u=d_u+\sum_{v\in\operatorname{children}(u)}c_v.

也就是说,cuc_u 是 uu 子树内所有标记之和。反过来,已知目标增量 cc 时,有

du=cu−∑v∈children⁡(u)cv. d_u=c_u-\sum_{v\in\operatorname{children}(u)}c_v.

这就是树上“累计”与“差分”的逆运算关系。这里将 dd 初始置零,cc 只表示修改增量,最终点权为 au+cua_u+c_u。

给节点 xx 到其祖先 uu 的整条路径加 vv,在哪里放标记?

在 xx 放 d[x] += v,这个贡献会一路传向根。要使它在经过 uu 后停止,就在 uu 的父节点放 d[fa[u]] -= v。

路径 x→ux\to u 上每个节点的子树只包含正标记,得到 +v+v;uu 的严格祖先同时包含正负两个标记,增量抵消;其他节点不受影响。如果 uu 是根,则没有需要抵消的祖先,跳过第二个标记。

如果路径两端 x,yx,y 没有祖先关系呢?

设 w=LCA⁡(x,y)w=\operatorname{LCA}(x,y) 为最近公共祖先。路径由 x→wx\to w 和 y→wy\to w 两条向上的链组成,但交汇点 ww 只应增加一次。于是做四处标记:

// 点权路径加;w 已通过 LCA 查询得到
// fa[root] = 0,根没有父节点
d[x] += v;
d[y] += v;
d[w] -= v;
if (fa[w] != 0) d[fa[w]] -= v;

两份正标记在 ww 汇合,减去一份后恰好保留 vv;在 ww 的父节点再减一份,使更高祖先处的总增量为 00。这也覆盖 x=yx=y、一端是另一端祖先、ww 为根等情况。

全部修改标记完成后,自底向上汇总一次 dd,就能得到每个点的总增量:

// order 是预先得到的后序序列,保证孩子先于父亲
for (int u : order) {
    if (fa[u] != 0) d[fa[u]] += d[u];
}
for (int u = 1; u <= n; ++u) {
    b[u] = a[u] + d[u];
}

已知 LCA 后,每次修改只需 O(1)O(1) 次标记;最后汇总为 O(n)O(n)。若用倍增查询 LCA,包含预处理的 qq 次路径修改总时间为 O(nlog⁡n+qlog⁡n+n)O(n\log n+q\log n+n)。这里是先完成所有修改,再统一查询最终点权的离线做法,并且公式针对点权路径加;边权路径加的抵消位置不同。

从序列到树,差分都在做同一件事:沿确定方向传递贡献,在目标范围之外放抵消标记,最后统一累计还原。

6 练习题目

7 相关题解

题目 OJ 难度
区间最大和 roj 普及+/提高-