学习笔记:全局平衡二叉树 GBBT

· · 算法·理论

叫法好像不少,我就按字面意思翻译成 Global Balanced Binary Tree 了,取首字母缩写 GBBT。

建议先了解重链剖分算法。
我写过一篇笔记,也可以看 OI Wiki。

下面默认读者已经了解了重链剖分。

无特殊说明时,可以简单认为维护的信息合并是 O(1) 的。

引入

先考虑一个树上问题:

我们知道,通过树链剖分可以把树上的一条路径转化成 dfs 序上 O(\log n) 个区间,再使用维护区间数据结构做这些操作即可。

以本题为例,把边权转到点上,使用线段树维护,一次操作的复杂度可以做到时间 O(\log^2 n),同时空间是 O(n) 的。

如果你还会一个常数优化技巧:由于线段树只需要维护链,可以对每一条链单独开一棵线段树,做到 O(\sum \log len),其中 len 是路径上一条重链的长度。

显然这样常数会变小,但是复杂度还是 O(\log^2 n) 的。

:::info[卡满复杂度的数据构造] 建立一棵 \sqrt n 个结点的二叉树。
对于每个结点到其儿子的边,我们将其替换成一条长度为 \sqrt n 长度的链。

这样一个节点到根平均会有 \frac{\log_2 n}{2} 个重链,每一个重链的长度至少是 O(\sqrt n) 的,线段树一次操作复杂度是 O(\log \sqrt n)=O(\log n) 的。

这样就被卡到了 O(\log^2 n) 的复杂度。

不过还是可以看出来,常数比直接在全局的线段树上查询要小。

使用这一种卡常方法,我们可以在一个线段树形成的“树”上描述一次操作的过程。

比如下图为一棵树和它的树链剖分,其中红边是重边,黑边是轻边。

下图是线段树形成的树。
其中一个线段树叶子上的数字是该位置对应原树上的节点编号。
重链链顶的轻边用虚线表示。

考虑操作的过程。

在路径 u,v 上面操作,就需要从 u,v 向上跳,做 O(\log n) 次重链上面的前缀操作,和 \operatorname{lca}(u,v) 所在重链上面的区间操作。

做一次 [top_u,u] 的前缀操作,时间是 O(d_u),其中 d_u 是 u 在维护 u 重链的线段树上的深度,值是 O(\log len)。

做一次区间操作,时间是 O(h_{top_u}),其中 h_{top_u} 是 u 所在重链的线段树的高度,值是 O(\log len)。

考虑利用这个性质进行优化。

全局平衡二叉树 / Global Balanced Binary Tree (GBBT)

GBBT 是对每个重链开一棵线段树的改进,或者说是静态化的 LCT。

GBBT 的改进在于,对于每一条重链同样使用了二叉树维护,但是二叉树对链上区间根据权值进行划分,做到按照引入中方法连接起来的总体树高是 O(\log n) 的,并可以支持 O(n \log n) 的建树和 O(\log n) 的路径操作。

GBBT 中,一棵重链的二叉树维护这条重链按照深度从小到大排序的所有节点,并且按照权值划分子树。

GBBT 的二叉树可以选择 nodey 和 leafy,两种各有优劣。

:::info[什么是 nodey 和 leafy] nodey-tree 的每一个节点维护节点本身的信息和子树的信息,每一个位置对应着一个节点的值和一个区间的值。
常见的 Treap、Splay 等一般也都是 nodey-tree。

leafy-tree 的每一个非叶子节点代表一个区间,只在叶子节点存值。
常见的线段树、WBLT 等都是 leafy-tree。 :::

nodey 的好处是空间是 1 倍的,坏处是一次 pushup 需要做两次信息合并,并且比较难写。
leafy 的好处是 pushup 只需要做一次合并,而且好写,坏处是空间 2 倍,并且高度也是 2 倍。

综合来看其实 nodey 更优秀一点(时空常数上),但是 leafy 很好写。

如果是特殊的操作(例如路径有一个端点是根),可以写 nodey,代码可以做到比较简洁。
nodey 的实现可以参考 OI wiki,这里介绍 leafy 的写法。

注:下文中 leafy-tree 指的是维护一条重链的二叉树,GBBT 指全局的二叉树。

在 GBBT 中,一个节点 u 的权值是 sz_u-sz_{son_u},即 u 的轻子树大小和加上 u 本身的大小。
(这里认为 sz_0=0,叶子的重儿子为 0。)

对于一条重链,按照权值不断将链划分。
每次取带权中点将当前的区间划分成两个区间,使得两个区间的权值都在总权值的一半左右,最后得到一棵 leafy-tree。

下图为引入中图示树的每个节点的权值。

这棵树按照权值划分和线段树一致,但是只是特殊情况。

可以证明,如果把这些二叉树按照引入中的方式连接起来,全局的树高是 O(\log n) 的。

这里记二叉树上面的边是实边,连接重链二叉树之间的边(原树上的轻边)是虚边。

::::info[证明] 记 W_p 为当前 leafy-tree 上节点 p 子树中叶子的权值之和。

从这个二叉树上向上跳,如果是跳了一条实边,那么 W_p 会翻倍。
如果跳了一条虚边,W_p 不会下降。
:::info[为什么跳虚边权值不会下降] 跳之前的 W_p 是一条重链上的权值之和,其值等于链顶的子树大小。

::: 由于一个 leafy-tree 点的 $W_p$ 最多是 $n$(根所在重链的 leafy-tree 的根),那么经过的实边最多会有 $O(\log n)$ 条。 又由于轻边的性质,虚边最多有 $O(\log n)$ 条。 所以这棵树的高度是 $O(\log n)$ 的。 :::: 这里给出建树的实现和解释。 #### 预处理 ```cpp vector<pair<int, int>> g[N]; int fa[N], wt[N], dep[N], sz[N], son[N]; void dfs1(int u, int pr){ fa[u] = pr; dep[u] = dep[pr] + 1; sz[u] = 1; for (auto [v, w] : g[u]) if (v != pr){ wt[v] = w; dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; } } ``` 和重链剖分一致,不多介绍。 #### 建树 ```cpp struct node { int ls, rs;//左右儿子的编号 int mid;//区间中点 int mx;//区间最大值 int t1, t2;//t1 为加标记,t2 为赋值标记 void add(int x){//区间加 mx += x; t1 += x; } void chg(int x){//区间赋值 mx = x; t1 = 0; t2 = x; } } t[N<<1];//注意 leafy-tree 需要两倍空间 int rt[N], tot; //rt[u] 是 u 所在重链的 leafy-tree 的根节点编号,tot 为目前 GBBT 的总节点数 #define lc t[p].ls #define rc t[p].rs int top[N], pos[N], len[N]; //top[u] 为 u 所在链顶 //pos[u] 是 u 在重链的 leafy-tree 上的位置(中序遍历的序号,或深度排名) //len[u] 是 u 所在重链的长度 void up(int p){//pushup t[p].mx = max(t[lc].mx, t[rc].mx);//由于没有的儿子编号是 0,而 t[0].mx=0,所以计算 0 不会影响信息正确性 } ``` 这部分用来存 GBBT,需要注意 leafy-tree 需要两倍空间。 建树时,从根开始 dfs,先对 $u$ 重链上所有节点的轻儿子递归建树,再把 $u$ 的重链拿出来建树。 同时需要计算出 $u$ 重链上的信息。 ```cpp int nod[N], cnt; //nod[i] 用于临时保存 u 所在的重链,cnt 是当前重链长度 void dfs2(int u){ for (int x = u; x; x = son[x]) for (auto [y, w] : g[x]) if (y != fa[x] && y != son[x]) dfs2(y);//递归重链上节点的轻儿子 cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x, pos[x] = cnt;//按照深度顺序加入重链节点,更新 pos[x] nod[cnt + 1] = 0;//重置哨兵 int root = build(1, cnt); for (int x = u; x; x = son[x]) rt[x] = root, top[x] = u, len[x] = cnt;//更新信息 } ``` 其中 `build(l,r)` 用于建出 $[nod_l , nod_r]$ 这一段链的二叉树,返回根节点的编号。 下面是 `build(l,r)` 的实现。 ```cpp int build(int l, int r){ if (l == r){//leafy-tree 上叶子 int p = ++tot; t[p].mid = l; t[p].mx = wt[nod[l]];//构建叶子 t[p].t2 =-1;//覆盖标记初始化为特殊值 return p; } int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } if (L == r)--L;//注意特判,不能有空区间 int p = ++tot; t[p].mid = L; lc = build(l, L), rc = build(L + 1, r);//递归左右区间建树 up(p);//更新信息 t[p].t2 =-1;//覆盖标记初始化为特殊值 return p; } ``` 注意二分的写法,由于一个结点的权值是 $sz_u-sz_{son_u}$,所以 $sz_{nod_i}$ 其实是重链上权值的后缀和。 全局数组 `sz[0]` 被初始化为 $0$,所以需要重置 `nod[cnt+1]=0`。 可以简化一点代码。 这样写的话,一条重链的操作就是简单的,和线段树几乎一模一样,只需要把 $mid$ 改成 leafy-tree 的带权中点即可。 :::info[例题实现] ```cpp #define mid t[p].mid void add(int p, int pl, int pr, int l, int r, int x){ if (l <= pl && pr <= r) return t[p].add(x); down(p); if (l <= mid) add(lc, pl, mid, l, r, x); if (r > mid) add(rc, mid + 1, pr, l, r, x); up(p); } void cov(int p, int pl, int pr, int l, int r, int x){ if (l <= pl && pr <= r) return t[p].chg(x); down(p); if (l <= mid) cov(lc, pl, mid, l, r, x); if (r > mid) cov(rc, mid + 1, pr, l, r, x); up(p); } int query(int p, int pl, int pr, int l, int r){ if (l <= pl && pr <= r) return t[p].mx; int res = 0; down(p); if (l <= mid) res = max(res, query(lc, pl, mid, l, r)); if (r > mid) res = max(res, query(rc, mid + 1, pr, l, r)); return res; } ``` ::: 路径操作和重链剖分也几乎一样,只需要更改查询的参数,在 leafy-tree 上面查询。 :::info[例题实现] ```cpp void add_link(int u, int v, int x){ while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); add(rt[u], 1, len[u], 1, pos[u], x); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) add(rt[u], 1, len[u], pos[u] + 1, pos[v], x); } void cov_link(int u, int v, int x){ while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); cov(rt[u], 1, len[u], 1, pos[u], x); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) cov(rt[u], 1, len[u], pos[u] + 1, pos[v], x); } int qry_link(int u, int v){ int res = 0; while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); res = max(res, query(rt[u], 1, len[u], 1, pos[u])); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) res = max(res, query(rt[u], 1, len[u], pos[u] + 1, pos[v])); return res; } ``` ::: 以下给出例题的完整实现: :::info[Code] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) const int N = 1e5 + 5, INF = 0x3f3f3f3f; int n; int eu[N], ev[N]; vector<pair<int, int>> g[N]; int fa[N], wt[N], dep[N], sz[N], son[N]; //这部分和重剖一致 void dfs1(int u, int pr){ fa[u] = pr; dep[u] = dep[pr] + 1; sz[u] = 1; for (auto [v, w] : g[u]) if (v != pr){ wt[v] = w; dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; } } struct node { int ls, rs; //左右儿子的编号 int mid; //区间中点 int mx; //区间最大值 int t1, t2; //t1 为加标记,t2 为赋值标记 void add(int x){//区间加 mx += x; t1 += x; } void chg(int x){//区间赋值 mx = x; t1 = 0; t2 = x; } } t[N << 1]; //注意 leafy-tree 需要两倍空间 int rt[N], tot; //rt[u] 是 u 所在重链的 leafy-tree 的根节点编号,tot 为目前 GBBT 的总节点数 #define lc t[p].ls #define rc t[p].rs int top[N], pos[N], len[N]; //top[u] 为 u 所在链顶 //pos[u] 是 u 在重链的 leafy-tree 上的位置(中序遍历的序号,或深度排名) //len[u] 是 u 所在重链的长度 void up(int p){//pushup t[p].mx = max(t[lc].mx, t[rc].mx); //由于没有的儿子编号是 0,而 t[0].mx=0,所以计算 0 不会影响信息正确性 } void down(int p){//pushdown,注意这里 lazy-tag 是先赋值再加的形式 if (t[p].t2 !=-1){ if (lc) t[lc].chg(t[p].t2); if (rc) t[rc].chg(t[p].t2); t[p].t2 =-1; } if (t[p].t1){ if (lc) t[lc].add(t[p].t1); if (rc) t[rc].add(t[p].t1); t[p].t1 = 0; } } int nod[N], cnt; //nod[i] 用于临时保存 u 所在的重链,cnt 是当前重链长度 int build(int l, int r){ if (l == r){//leafy-tree 上叶子 int p = ++tot; t[p].mid = l; t[p].mx = wt[nod[l]]; //构建叶子 t[p].t2 =-1; //覆盖标记初始化为特殊值 return p; } int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } if (L == r)--L; //注意特判,不能有空区间 int p = ++tot; t[p].mid = L; lc = build(l, L), rc = build(L + 1, r); //递归左右区间建树 up(p); //更新信息 t[p].t2 =-1; //覆盖标记初始化为特殊值 return p; } void dfs2(int u){ for (int x = u; x; x = son[x]) for (auto [y, w] : g[x]) if (y != fa[x] && y != son[x]) dfs2(y); //递归重链上节点的轻儿子 cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x, pos[x] = cnt; //按照深度顺序加入重链节点,更新 pos[x] nod[cnt + 1] = 0; //重置哨兵 int root = build(1, cnt); for (int x = u; x; x = son[x]) rt[x] = root, top[x] = u, len[x] = cnt; //更新信息 } #define mid t[p].mid//mid 是区间带权中点,同时是左区间右端点 //以下的实现和普通的线段树几乎一致 void add(int p, int pl, int pr, int l, int r, int x){ if (l <= pl && pr <= r) return t[p].add(x); down(p); if (l <= mid) add(lc, pl, mid, l, r, x); if (r > mid) add(rc, mid + 1, pr, l, r, x); up(p); } void cov(int p, int pl, int pr, int l, int r, int x){ if (l <= pl && pr <= r) return t[p].chg(x); down(p); if (l <= mid) cov(lc, pl, mid, l, r, x); if (r > mid) cov(rc, mid + 1, pr, l, r, x); up(p); } int query(int p, int pl, int pr, int l, int r){ if (l <= pl && pr <= r) return t[p].mx; int res = 0; down(p); if (l <= mid) res = max(res, query(lc, pl, mid, l, r)); if (r > mid) res = max(res, query(rc, mid + 1, pr, l, r)); return res; } //以下的实现和树剖+线段树也几乎一致 void add_link(int u, int v, int x){ while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); add(rt[u], 1, len[u], 1, pos[u], x); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) add(rt[u], 1, len[u], pos[u] + 1, pos[v], x); } void cov_link(int u, int v, int x){ while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); cov(rt[u], 1, len[u], 1, pos[u], x); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) cov(rt[u], 1, len[u], pos[u] + 1, pos[v], x); } int qry_link(int u, int v){ int res = 0; while (top[u] != top[v]){ if (dep[top[u]] < dep[top[v]]) swap(u, v); res = max(res, query(rt[u], 1, len[u], 1, pos[u])); u = fa[top[u]]; } if (dep[u] > dep[v]) swap(u, v); if (u != v) res = max(res, query(rt[u], 1, len[u], pos[u] + 1, pos[v])); return res; } int main(){ ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n; rep(i, 1, n - 1){ int u, v, w; cin >> u >> v >> w; eu[i] = u, ev[i] = v; g[u].push_back({v, w}); g[v].push_back({u, w}); } dfs1(1, 0); dfs2(1); string op; int x, y, z; while (cin >> op){ if (op == "Stop") break; if (op == "Change"){ cin >> x >> y; cov_link(eu[x], ev[x], y); } else if (op == "Cover"){ cin >> x >> y >> z; cov_link(x, y, z); } else if (op == "Add"){ cin >> x >> y >> z; add_link(x, y, z); } else { cin >> x >> y; cout << qry_link(x, y) << '\n'; } } return 0; } ``` ::: ### 复杂度证明 以上面例题的代码为例,nodey-tree 的证明也是类似的。 #### 建树 如果设 $len$ 是当前区间的长度,$k$ 是左区间的长度,那么有 $T(len)=O(\log len)+T(k)+T(len-k)$。 由于 $k$ 的值没有保证,但是 $W$ 是减半的,所以最多只会递归 $O(\log n)$ 层。 记每一层的若干个区间长度是 $L_i$。 那么一层的复杂度就是 $O(\sum \log L_i) \leq O(len)$。 所以建立一条重链的 leafy-tree 是 $O(len \log n)$ 的。 所以复杂度最坏 $O(\sum len \log n)=O(n \log n)$。 #### 路径操作 记 $d_u$ 为 GBBT 上节点 $u$ 的深度,$rt_u$ 为节点 $u$ 所在重链的 leafy-tree 的根节点编号。 那么重链上一次 $[top_u,u]$ 前缀操作的时间复杂度就是 $O(d_u-d_{rt_u})$。 因为如果 $[top_u,u]$ 在当前左区间内,那么会直接递归左区间;否则递归的左区间会被完全包含,直接返回,然后再递归右区间。 这样经过的节点总数就是 $rt_u$ 和 $u$ 对应叶子的距离,就是 $O(d_u-d_{rt_u})$。 本质上就是从 $rt_u$ 走到 $u$ 对应的叶子。 而对于 $u$ 到 $\operatorname{lca}(u,v)$ 上面的所有重链(不包含 $\operatorname{lca}(u,v)$),这部分的复杂度是 $O(h)$ 的。 其中 $h$ 是 GBBT 的树高,根据上文可以得到为 $O(\log n)$。 根据上面的分析,得到一次前缀查询是从 $u$ 对应的叶子走到 $rt_u$ 的过程,而所有走过的路径在 GBBT 上面组成了一条从一个节点向根方向的链,长度不超过树的高度,所以是 $O(h)$ 的。 因此这一部分的时间复杂度是 $O(\log n)$。 $v$ 到 $\operatorname{lca}(u,v)$ 上面的所有重链同理。 $\operatorname{lca}(u,v)$ 的重链做一次区间查询,复杂度是 $O(\log len) \leq O(\log n)$ 的。 因此一次路径操作的时间复杂度是 $O(\log n)$。 ### 拓展 #### 子树操作 在 GBBT 上进行子树操作是很复杂的,一般不建议用 GBBT 写同时有路径操作和子树操作的题。 但是理论上 GBBT 是可以同时维护简单的路径 / 子树操作的,例如树剖模板题。 可以阅读[文章](https://www.luogu.com.cn/article/tul5v12m)了解。 #### 可持久化 参考[该文章](https://www.luogu.com.cn/article/88v1dia4)。 ### 习题 见[题单](https://www.luogu.com.cn/training/851940),大部分路径操作题都可以拿来练手。 ## GBBT 优化动态 DP 对于路径类 DDP 的实现,其实和维护路径信息是一样的,不再介绍。 对于子树类 DDP 的实现,其实是一个只有到根路径修改的特化,本质仍然和维护路径信息相同。 只有到根路径修改的情况下,可以直接使用迭代式的修改,使用 nodey 和 leafy 都很简单。 这里以 [P4751 【模板】动态 DP(加强版)](https://www.luogu.com.cn/problem/P4751) 为例。 如果你没有学过动态 DP,可以在我的[重链剖分学习笔记](https://www.luogu.com.cn/article/z62murs1)中了解。 建树部分和维护普通信息是一致的,因此只介绍修改部分。 这里给出 nodey-tree 和 leafy-tree 的实现。 ::::info[nodey-tree] ```cpp struct node { int ls, rs, pr;//左右儿子和父节点 mat val, mul;//该节点的矩阵和 nodey-tree 的子树乘积 } t[N]; #define lc t[p].ls #define rc t[p].rs inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[p].val * t[lc].mul : t[p].val;}//pushup需要计算节点本身的值和左右子树的值 //pushup 特判叶子节点可以减小常数 ``` ```cpp inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); }//x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1] void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 t[u].val.a[1][0] += x - a[u];//更新 u 的转移矩阵 a[u] = x;//更新 a[u] while (u){ int p = t[u].pr; //p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul);//旧的贡献值 up(u);//更新 u get(nf1, nf0, t[u].mul);//新的贡献值 int d0 = max(nf1, nf0) - max(f1, f0), d1 = nf0 - f0; t[p].val.a[0][1] = t[p].val.a[1][1] += d0; t[p].val.a[1][0] += d1;//更新 p 的转移矩阵 } else up(u);//更新信息 u = p; } } ``` :::info[完整代码] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) const int N = 1e6 + 5, INF = 0x3f3f3f3f; int n, m; int a[N]; vector<int> e[N]; int fa[N], sz[N], dep[N], son[N]; int f[N][2]; void dfs1(int u, int pr){ fa[u] = pr; sz[u] = 1; dep[u] = dep[pr] + 1; f[u][1] = a[u]; for (int v : e[u]) if (v != pr){ dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } struct mat { int a[2][2]; }; const mat I = {{{0,-INF}, {-INF, 0}}}; //单位矩阵 const mat E = {{{-INF,-INF}, {-INF,-INF}}}; //空矩阵 const mat F0 = {{{0, 0}, {-INF,-INF}}}; //初始的转移矩阵 inline mat operator * (const mat & x, const mat & y){ return {{ {max(x.a[0][0] + y.a[0][0], x.a[0][1] + y.a[1][0]), max(x.a[0][0] + y.a[0][1], x.a[0][1] + y.a[1][1])}, {max(x.a[1][0] + y.a[0][0], x.a[1][1] + y.a[1][0]), max(x.a[1][0] + y.a[0][1], x.a[1][1] + y.a[1][1])} } }; } //使用了矩乘展开以减小常数 struct node { int ls, rs, pr; //左右儿子和父节点 mat val, mul; //该节点的矩阵和 nodey-tree 的子树乘积 } t[N]; #define lc t[p].ls #define rc t[p].rs inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[p].val * t[lc].mul : t[p].val;}//pushup需要计算节点本身的值和左右子树的值 //pushup 特判叶子节点可以减小常数 int nod[N], cnt; int build(int l, int r){ if (l > r) return 0; int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } int x = L, p = nod[x]; lc = build(l, x - 1), rc = build(x + 1, r); if (lc) t[lc].pr = p; if (rc) t[rc].pr = p; up(p); return p; } int dfs2(int u){//需要处理轻儿子对 dp 的贡献,这里直接在节点的矩阵上面修改 for (int x = u; x; x = son[x]) t[x].val.a[1][0] = a[x], t[x].val.a[0][0] =-INF; for (int x = u; x; x = son[x]) for (int y : e[x]) if (y != fa[x] && y != son[x]){ t[dfs2(y)].pr = x; t[x].val.a[0][1] = t[x].val.a[1][1] += max(f[y][0], f[y][1]); t[x].val.a[1][0] += f[y][0]; } cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x; nod[cnt + 1] = 0; return build(1, cnt); } inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); } //x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1] void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 t[u].val.a[1][0] += x - a[u]; //更新 u 的转移矩阵 a[u] = x; //更新 a[u] while (u){ int p = t[u].pr; //p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul); //旧的贡献值 up(u); //更新 u get(nf1, nf0, t[u].mul); //新的贡献值 int d0 = max(nf1, nf0) - max(f1, f0), d1 = nf0 - f0; t[p].val.a[0][1] = t[p].val.a[1][1] += d0; t[p].val.a[1][0] += d1; //更新 p 的转移矩阵 } else up(u); //更新信息 u = p; } } int main(){ ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m; rep(i, 1, n) cin >> a[i]; rep(i, 2, n){ int u, v; cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs1(1, 0); t[0].mul = I; int rt = dfs2(1); int ans = 0; while (m--){ int u, x; cin >> u >> x; u ^= ans; updt(u, x); int f1, f0; get(f1, f0, t[rt].mul); ans = max(f1, f0); cout << ans << '\n'; } return 0; } ``` ::: :::: ::::info[leafy-tree] ```cpp struct node { int ls, rs, pr;//左右儿子和父节点 mat mul;//leafy-tree 的子树乘积 } t[N<<1]; int tot;// GBBT 的叶子编号为节点编号,新增节点编号由 tot 记录 #define lc t[p].ls #define rc t[p].rs inline mat getmat(int u){return {{{-INF, g[u][0]}, {g[u][1], g[u][0]}}};} inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[lc].mul : getmat(p);}//需要特判 p 是否为叶子 ``` ```cpp inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); }//x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1],写这个函数是为了减少常数 void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 g[u][1] += x - a[u];//更新 u 的转移矩阵 a[u] = x;//更新 a[u] while (u){ int p = t[u].pr;//p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul);//旧的贡献值 up(u);//更新 u get(nf1, nf0, t[u].mul);//新的贡献值 g[p][0] += max(nf1, nf0) - max(f1, f0), g[p][1] += nf0 - f0;//更新 p 的转移矩阵 } else up(u);//更新信息 u = p; } } ``` :::info[完整代码] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) const int N = 1e6 + 5, INF = 0x3f3f3f3f; int n, m; int a[N]; vector<int> e[N]; int fa[N], sz[N], dep[N], son[N]; int f[N][2], g[N][2]; void dfs1(int u, int pr){ fa[u] = pr; sz[u] = 1; dep[u] = dep[pr] + 1; f[u][1] = a[u]; for (int v : e[u]) if (v != pr){ dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } struct mat { int a[2][2]; }; const mat I = {{{0,-INF}, {-INF, 0}}}; //单位矩阵 const mat E = {{{-INF,-INF}, {-INF,-INF}}}; //空矩阵 const mat F0 = {{{0, 0}, {-INF,-INF}}}; //初始的转移矩阵 inline mat operator * (const mat & x, const mat & y){ return {{ {max(x.a[0][0] + y.a[0][0], x.a[0][1] + y.a[1][0]), max(x.a[0][0] + y.a[0][1], x.a[0][1] + y.a[1][1])}, {max(x.a[1][0] + y.a[0][0], x.a[1][1] + y.a[1][0]), max(x.a[1][0] + y.a[0][1], x.a[1][1] + y.a[1][1])} } }; } //使用了矩乘展开以减小常数 struct node { int ls, rs, pr; //左右儿子和父节点 mat mul; //leafy-tree 的子树乘积 } t[N<<1]; int tot; // GBBT 的叶子编号为节点编号,新增节点编号由 tot 记录 #define lc t[p].ls #define rc t[p].rs inline mat getmat(int u){return {{{-INF, g[u][0]}, {g[u][1], g[u][0]}}};} inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[lc].mul : getmat(p);}//需要特判 p 是否为叶子 int nod[N], cnt; int build(int l, int r){ if (l == r){ int p = nod[l]; t[p].mul = getmat(p); return p; } int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } if (L == r)--L; int x = L, p = ++tot; lc = build(l, x), rc = build(x + 1, r); if (lc) t[lc].pr = p; if (rc) t[rc].pr = p; up(p); return p; } int dfs2(int u){ for (int x = u; x; x = son[x]) g[x][1] = a[x]; for (int x = u; x; x = son[x]) for (int y : e[x]) if (y != fa[x] && y != son[x]){ t[dfs2(y)].pr = x; g[x][0] += max(f[y][0], f[y][1]); g[x][1] += f[y][0]; } cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x; nod[cnt + 1] = 0; return build(1, cnt); } inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); } //x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1],写这个函数是为了减少常数 void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 g[u][1] += x - a[u]; //更新 u 的转移矩阵 a[u] = x; //更新 a[u] while (u){ int p = t[u].pr; //p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul); //旧的贡献值 up(u); //更新 u get(nf1, nf0, t[u].mul); //新的贡献值 g[p][0] += max(nf1, nf0) - max(f1, f0), g[p][1] += nf0 - f0; //更新 p 的转移矩阵 } else up(u); //更新信息 u = p; } } int main(){ ios::sync_with_stdio(0); cin.tie(0), cout.tie(0); cin >> n >> m; rep(i, 1, n) cin >> a[i]; rep(i, 2, n){ int u, v; cin >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs1(1, 0); t[0].mul = I; tot = n; int rt = dfs2(1); int ans = 0; while (m--){ int u, x; cin >> u >> x; u ^= ans; updt(u, x); int f1, f0; get(f1, f0, t[rt].mul); ans = max(f1, f0); cout << ans << '\n'; } return 0; } ``` ::: :::: 可以发现,实现都很简单,但是 nodey-tree 更加简洁。 并且从评测结果来看,[nodey-tree](https://www.luogu.com.cn/record/300764995) 的时空常数都比 [leafy-tree](https://www.luogu.com.cn/record/300764021) 小(也有可能是我的实现问题),所以 nodey-tree 是比较推荐的写法。 根据代码很容易分析,单次修改的复杂度是 $O(h)=O(\log n)$ 的。 ### 习题 见[题单](https://www.luogu.com.cn/training/1007000)。 ### 附录:性能对比 下面是本地的测试结果对比。 使用的是 DDP 数据加强版,为了方便去掉了强制在线,但是代码都是在线的。 为了体现常数和复杂度差异,测试数据为 $n=4 \times 10^6$,$m=10^7$。 树的结构是完全二叉树套链。 :::info[Gen] ```cpp #include<bits/stdc++.h> #include <chrono> using namespace std; #define ll long long #define rep(a,b,c) for(int a=(b);a<=(c);++a) #define per(a,b,c) for(int a=(b);a>=(c);--a) struct io { #define ll long long #define lll __int128 #define SZ 1<<21 char buf[SZ],*p1,*p2; char pbuf[SZ],*pp; io() : p1(buf), p2(buf), pp(pbuf){} ~io(){fwrite(pbuf, 1, pp - pbuf, stdout);} inline char gc(){ if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, SZ, stdin); return p1 == p2 ? EOF :*p1++; } #define isdigit(ch) (ch>47&&ch<58) inline io & operator >>(int & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline io & operator >>(ll & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline io & operator >>(lll & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline io & operator >>(char & c){ c = gc(); for (; isspace(c);) c = gc(); return * this; } inline io & operator >>(string & s){ s = ""; char c = gc(); for (; isspace(c);) c = gc(); for (; !isspace(c) && c != EOF; c = gc()) s += c; return * this; } inline void push(const char & c){ if (pp - pbuf == SZ) fwrite(pbuf, 1, SZ, stdout), pp = pbuf; *pp++ = c; } int stk[45], top; inline io & operator << (int x){ if (!x){push('0'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); return * this; } inline io & operator << (ll x){ if (!x){push('0'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); return * this; } inline io & operator << (lll x){ if (!x){push('0'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); return * this; } inline io & operator << (const char & c){ push(c); return * this; } inline io & operator << (const string & s){ for (char c : s) push(c); return * this; } } IO; mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); int rnd(int l, int r){ return rng() % (r - l + 1) + l; } const int N = 4e6, M = 1e7; int n = N, m = M; #define linkfa(i) (((i)-1)*2000+1) int main(){ freopen("DDP.in", "w", stdout); IO << n << ' ' << m << '\n'; rep(u, 1, n) IO << rnd(-100, 100) << " \n"[u == n]; rep(i, 1, 2000){ if (i != 1) IO << linkfa(i) << ' ' << linkfa(i >> 1) + 1999 << '\n'; rep(j, 1, 1999) IO << linkfa(i) + j - 1 << ' ' << linkfa(i) + j << '\n'; } rep(i, 1, m){ int u = linkfa(rnd(1025, 2000)) + rnd(1000, 1999), x = rnd(-100, 100); IO << u << ' ' << x << '\n'; } return 0; } ``` ::: 每一个代码测试三次,运行用时和峰值内存可供参考。 Nodey-tree: 5042.774 ms/447896 KB 5005.918 ms/447900 KB 5022.028 ms/447896 KB :::info[Code] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) struct io { #define SZ 1<<21 char buf[SZ], pbuf[SZ]; char * p1,*p2,*pp; io() : p1(buf), p2(buf), pp(pbuf){} ~io(){fwrite(pbuf, 1, pp - pbuf, stdout);} inline char gc(){ if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, SZ, stdin); return p1 == p2 ? EOF :*p1++; } #define isdigit(ch) (ch>47&&ch<58) template<typename T>inline io & operator >>(T & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline void push(const char & c){ if (pp - pbuf == SZ) fwrite(pbuf, 1, SZ, stdout), pp = pbuf; *pp++ = c; } int stk[45], top; template<typename T>inline io & operator << (T x){ if (!x){push('0'); push('\n'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); push('\n'); return * this; } } IO; const int N = 4e6 + 5, INF = 0x3f3f3f3f; int n, m; int a[N]; vector<int> e[N]; int fa[N], sz[N], dep[N], son[N]; int f[N][2]; void dfs1(int u, int pr){ fa[u] = pr; sz[u] = 1; dep[u] = dep[pr] + 1; f[u][1] = a[u]; for (int v : e[u]) if (v != pr){ dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } struct mat { int a[2][2]; }; const mat I = {{{0,-INF}, {-INF, 0}}}; //单位矩阵 const mat E = {{{-INF,-INF}, {-INF,-INF}}}; //空矩阵 const mat F0 = {{{0, 0}, {-INF,-INF}}}; //初始的转移矩阵 inline mat operator * (const mat & x, const mat & y){ return {{ {max(x.a[0][0] + y.a[0][0], x.a[0][1] + y.a[1][0]), max(x.a[0][0] + y.a[0][1], x.a[0][1] + y.a[1][1])}, {max(x.a[1][0] + y.a[0][0], x.a[1][1] + y.a[1][0]), max(x.a[1][0] + y.a[0][1], x.a[1][1] + y.a[1][1])} } }; } //使用了矩乘展开以减小常数 struct node { int ls, rs, pr; //左右儿子和父节点 mat val, mul; //该节点的矩阵和 nodey-tree 的子树乘积 } t[N]; #define lc t[p].ls #define rc t[p].rs inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[p].val * t[lc].mul : t[p].val;}//pushup需要计算节点本身的值和左右子树的值 //pushup 特判叶子节点可以减小常数 int nod[N], cnt; int build(int l, int r){ if (l > r) return 0; int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } int x = L, p = nod[x]; lc = build(l, x - 1), rc = build(x + 1, r); if (lc) t[lc].pr = p; if (rc) t[rc].pr = p; up(p); return p; } int dfs2(int u){//需要处理轻儿子对 dp 的贡献,这里直接在节点的矩阵上面修改 for (int x = u; x; x = son[x]) t[x].val.a[1][0] = a[x], t[x].val.a[0][0] =-INF; for (int x = u; x; x = son[x]) for (int y : e[x]) if (y != fa[x] && y != son[x]){ t[dfs2(y)].pr = x; t[x].val.a[0][1] = t[x].val.a[1][1] += max(f[y][0], f[y][1]); t[x].val.a[1][0] += f[y][0]; } cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x; nod[cnt + 1] = 0; return build(1, cnt); } inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); } //x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1] void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 t[u].val.a[1][0] += x - a[u]; //更新 u 的转移矩阵 a[u] = x; //更新 a[u] while (u){ int p = t[u].pr; //p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul); //旧的贡献值 up(u); //更新 u get(nf1, nf0, t[u].mul); //新的贡献值 int d0 = max(nf1, nf0) - max(f1, f0), d1 = nf0 - f0; t[p].val.a[0][1] = t[p].val.a[1][1] += d0; t[p].val.a[1][0] += d1; //更新 p 的转移矩阵 } else up(u); //更新信息 u = p; } } int main(){ freopen("DDP.in", "r", stdin); freopen("DDP.out", "w", stdout); IO >> n >> m; rep(i, 1, n) IO >> a[i]; rep(i, 2, n){ int u, v; IO >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs1(1, 0); t[0].mul = I; int rt = dfs2(1); int ans = 0; while (m--){ int u, x; IO >> u >> x; //u ^= ans; updt(u, x); int f1, f0; get(f1, f0, t[rt].mul); ans = max(f1, f0); IO << ans; } return 0; } ``` ::: Leafy-tree: 6094.566 ms/541804 KB 6172.198 ms/526000 KB 6044.554 ms/526004 KB :::info[Code] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) struct io { #define SZ 1<<21 char buf[SZ], pbuf[SZ]; char * p1,*p2,*pp; io() : p1(buf), p2(buf), pp(pbuf){} ~io(){fwrite(pbuf, 1, pp - pbuf, stdout);} inline char gc(){ if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, SZ, stdin); return p1 == p2 ? EOF :*p1++; } #define isdigit(ch) (ch>47&&ch<58) template<typename T>inline io & operator >>(T & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline void push(const char & c){ if (pp - pbuf == SZ) fwrite(pbuf, 1, SZ, stdout), pp = pbuf; *pp++ = c; } int stk[45], top; template<typename T>inline io & operator << (T x){ if (!x){push('0'); push('\n'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); push('\n'); return * this; } } IO; const int N = 4e6 + 5, INF = 0x3f3f3f3f; int n, m; int a[N]; vector<int> e[N]; int fa[N], sz[N], dep[N], son[N]; int f[N][2], g[N][2]; void dfs1(int u, int pr){ fa[u] = pr; sz[u] = 1; dep[u] = dep[pr] + 1; f[u][1] = a[u]; for (int v : e[u]) if (v != pr){ dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } struct mat { int a[2][2]; }; const mat I = {{{0,-INF}, {-INF, 0}}}; //单位矩阵 const mat E = {{{-INF,-INF}, {-INF,-INF}}}; //空矩阵 const mat F0 = {{{0, 0}, {-INF,-INF}}}; //初始的转移矩阵 inline mat operator * (const mat & x, const mat & y){ return {{ {max(x.a[0][0] + y.a[0][0], x.a[0][1] + y.a[1][0]), max(x.a[0][0] + y.a[0][1], x.a[0][1] + y.a[1][1])}, {max(x.a[1][0] + y.a[0][0], x.a[1][1] + y.a[1][0]), max(x.a[1][0] + y.a[0][1], x.a[1][1] + y.a[1][1])} } }; } //使用了矩乘展开以减小常数 struct node { int ls, rs, pr; //左右儿子和父节点 mat mul; //leafy-tree 的子树乘积 } t[N<<1]; int tot; // GBBT 的叶子编号为节点编号,新增节点编号由 tot 记录 #define lc t[p].ls #define rc t[p].rs inline mat getmat(int u){return {{{-INF, g[u][0]}, {g[u][1], g[u][0]}}};} inline void up(int p){t[p].mul = rc || lc ? t[rc].mul * t[lc].mul : getmat(p);}//需要特判 p 是否为叶子 int nod[N], cnt; int build(int l, int r){ if (l == r){ int p = nod[l]; t[p].mul = getmat(p); return p; } int L = l, R = r; while (L < R){ int mid = (L + R) >> 1; if (2 * (sz[nod[l]] - sz[nod[mid + 1]]) < sz[nod[l]] - sz[nod[r + 1]]) L = mid + 1; else R = mid; } if (L == r)--L; int x = L, p = ++tot; lc = build(l, x), rc = build(x + 1, r); if (lc) t[lc].pr = p; if (rc) t[rc].pr = p; up(p); return p; } int dfs2(int u){ for (int x = u; x; x = son[x]) g[x][1] = a[x]; for (int x = u; x; x = son[x]) for (int y : e[x]) if (y != fa[x] && y != son[x]){ t[dfs2(y)].pr = x; g[x][0] += max(f[y][0], f[y][1]); g[x][1] += f[y][0]; } cnt = 0; for (int x = u; x; x = son[x]) nod[++cnt] = x; nod[cnt + 1] = 0; return build(1, cnt); } inline void get(int & x, int & y, const mat & ma){ x = max(ma.a[0][0], ma.a[1][0]); y = max(ma.a[0][1], ma.a[1][1]); } //x,y 分别为初始状态矩阵与 ma 相乘结果矩阵 M 的 M[0][0] 和 M[0][1],写这个函数是为了减少常数 void updt(int u, int x){//这里记录父节点可以直接一直向上跳直到跳到全局的根,实现会简单很多 g[u][1] += x - a[u]; //更新 u 的转移矩阵 a[u] = x; //更新 a[u] while (u){ int p = t[u].pr; //p 为 u 在 GBBT 上的父亲 if (p && lc != u && rc != u){//向上跳了一条轻边,需要更新父节点的值 int f1, f0, nf1, nf0; get(f1, f0, t[u].mul); //旧的贡献值 up(u); //更新 u get(nf1, nf0, t[u].mul); //新的贡献值 g[p][0] += max(nf1, nf0) - max(f1, f0), g[p][1] += nf0 - f0; //更新 p 的转移矩阵 } else up(u); //更新信息 u = p; } } int getht(int u){ int h = 0; for (; u; u = t[u].pr)++h; return h; } int main(){ freopen("DDP.in", "r", stdin); freopen("DDP.out", "w", stdout); IO >> n >> m; rep(i, 1, n) IO >> a[i]; rep(i, 2, n){ int u, v; IO >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs1(1, 0); t[0].mul = I; tot = n; int rt = dfs2(1); int ans = 0; while (m--){ int u, x; IO >> u >> x; //u ^= ans; updt(u, x); int f1, f0; get(f1, f0, t[rt].mul); ans = max(f1, f0); IO << ans; } return 0; } ``` ::: 树剖线段树: 12190.496 ms/561196 KB 11667.443 ms/561196 KB 12140.327 ms/561196 KB :::info[Code] ```cpp #include<bits/stdc++.h> using namespace std; #define rep(a,b,c) for(int a=(b);a<=(c);++a) const int N = 4e6 + 5, INF = 0x3f3f3f3f; struct io { #define SZ 1<<21 char buf[SZ], pbuf[SZ]; char * p1,*p2,*pp; io() : p1(buf), p2(buf), pp(pbuf){} ~io(){fwrite(pbuf, 1, pp - pbuf, stdout);} inline char gc(){ if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, SZ, stdin); return p1 == p2 ? EOF :*p1++; } #define isdigit(ch) (ch>47&&ch<58) template<typename T>inline io & operator >>(T & x){ x = 0; bool neg = 0; char ch = gc(); for (; !isdigit(ch); ch = gc()) if (ch == '-') neg = 1; if (neg) for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) - (ch ^ 48); else for (; isdigit(ch); ch = gc()) x = (x << 1) + (x << 3) + (ch ^ 48); return * this; } inline void push(const char & c){ if (pp - pbuf == SZ) fwrite(pbuf, 1, SZ, stdout), pp = pbuf; *pp++ = c; } int stk[45], top; template<typename T>inline io & operator << (T x){ if (!x){push('0'); push('\n'); return * this;} if (x < 0){push('-'); x =-x;} while (x) stk[++top] = x % 10, x /= 10; while (top) push(stk[top--] ^ 48); push('\n'); return * this; } } IO; int n, m; int a[N]; vector<int> e[N]; int fa[N], sz[N], dep[N], son[N]; int f[N][2], g[N][2]; void dfs1(int u, int pr){ fa[u] = pr; sz[u] = 1; dep[u] = dep[pr] + 1; f[u][1] = a[u]; for (int v : e[u]) if (v != pr){ dfs1(v, u); sz[u] += sz[v]; if (sz[son[u]] < sz[v]) son[u] = v; f[u][1] += f[v][0]; f[u][0] += max(f[v][0], f[v][1]); } } int top[N], bot[N], dfn[N], nod[N], cnt; struct mat { int a[2][2]; }; const mat I = {{{0,-INF}, {-INF, 0}}}; const mat E = {{{-INF,-INF}, {-INF,-INF}}}; const mat F0 = {{{0, 0}, {-INF,-INF}}}; mat operator * (const mat & x, const mat & y){ return {{ {max(x.a[0][0] + y.a[0][0], x.a[0][1] + y.a[1][0]), max(x.a[0][0] + y.a[0][1], x.a[0][1] + y.a[1][1])}, {max(x.a[1][0] + y.a[0][0], x.a[1][1] + y.a[1][0]), max(x.a[1][0] + y.a[0][1], x.a[1][1] + y.a[1][1])} } }; } struct node { mat mul; int lson, rson; } t[N<<2]; int rt[N], ct; #define ls t[p].lson #define rs t[p].rson #define mid ((pl+pr)>>1) mat getmat(int u){return {{{-INF, g[u][0]}, {g[u][1], g[u][0]}}};} void up(int p){t[p].mul = t[rs].mul * t[ls].mul;} void build(int & p, int pl, int pr){ p = ++ct; if (pl == pr){ t[p].mul = getmat(nod[pl]); return; } build(ls, pl, mid); build(rs, mid + 1, pr); up(p); } void dfs2(int u, int tp){ top[u] = tp; bot[u] = u; dfn[u] = ++cnt; nod[cnt] = u; g[u][1] = a[u]; if (!son[u]) return; dfs2(son[u], tp); bot[u] = bot[son[u]]; for (int v : e[u]) if (v != fa[u] && v != son[u]){ dfs2(v, v); g[u][0] += max(f[v][0], f[v][1]); g[u][1] += f[v][0]; } } int stk[20], st; void upd(int u){ int p = rt[top[u]], pl = dfn[top[u]], pr = dfn[bot[u]], x = dfn[u]; while (pl != pr){ stk[++st] = p; x <= mid ? (pr = mid, p = ls) : (pl = mid + 1, p = rs); } t[p].mul = getmat(u); while (st) up(stk[st--]); } void updt(int u, int x){ g[u][1] += x - a[u]; a[u] = x; while (u){ int tp = top[u], p = fa[tp]; mat F = F0 * t[rt[tp]].mul; int f1 = F.a[0][0], f0 = F.a[0][1]; upd(u); mat nF = F0 * t[rt[tp]].mul; int nf1 = nF.a[0][0], nf0 = nF.a[0][1]; if (p) g[p][0] += max(nf1, nf0) - max(f1, f0), g[p][1] += nf0 - f0; u = p; } } int main(){ freopen("DDP.in", "r", stdin); freopen("DDP.out", "w", stdout); IO >> n >> m; rep(i, 1, n) IO >> a[i]; rep(i, 2, n){ int u, v; IO >> u >> v; e[u].push_back(v); e[v].push_back(u); } dfs1(1, 0); dfs2(1, 1); rep(u, 1, n) if (top[u] == u) build(rt[u], dfn[u], dfn[bot[u]]); while (m--){ int u, x; IO >> u >> x; updt(u, x); mat res = F0 * t[rt[1]].mul; IO << max(res.a[0][0], res.a[0][1]); } return 0; } ``` ::: 以下为我的设备配置和编译选项: 处理器: AMD Ryzen 7 5800X3D 8-Core Processor 3.40 GHz 机带RAM: 32.0 GB 速度:2400 MHz 编译器: mingw gcc 15.1.0 (posix-seh-ucrt) Version: 15.1.0 Platform: Windows 编译选项: `-std=c++17 -O2 -static -Wl,--stack=1000000000 -Wshadow` 可以看出来两种 GBBT 的和树剖线段树的常数差距和复杂度优化。 upd on 2026/10/6: 原来的 `Gen.cpp` 写错了,数据不够强没卡满树剖,已更新。