Description
给出一棵边带权((c))的节点数量为 (n) 的树,初始树上所有节点都是白色。有两种操作:
-
C x
,改变节点 (x) 的颜色,即白变黑,黑变白。 -
A
,询问树中最远的两个白色节点的距离,这两个白色节点可以重合(此时距离为 (0))。
(q) 次操作,输出所有查询的答案。
Hint
- (1le n, qle 10^5)
- (0le |c|le 10^3)
Solution
此题使用轻重链剖分真的麻烦
先树剖,然后根据每一个重链,建出一棵线段树(最后建出的是线段树森而非一棵大线段树,动态开点实现)。设 (root(x)) 为结点 (x) 对应线段树的根。非链顶结点的 (root) 无意义。
线段树上的每个结点维护 (3) 个字段:
- (lx(x)) 结点 (x) 代表的链上一段区间的 左端点(深度小的)可以到达 以链顶为根的子树 中最远的白点的距离。
- (rx(x)) 结点 (x) 代表的链上一段区间的 右端点(深度大的)可以到达 以链顶为根的子树 中最远的白点的距离。
- (mx(x)) 结点 (x) 代表满足 LCA 在当前结点区间中的所有白点对 中最大的距离。
那么我们可以这样设计我们的 pushup
函数,注意此处的 (dep) 带边权。
#define dis(x) dep[pos[x]] // dis(i) 表示区间中位置 i 所对应结点的深度
void pushup(int x, int l, int r) { // x 为线段树上当前结点,对应区间为 [l, r]
lx[x] = max(lx[lc[x]], lx[rc[x]] + dis(mid + 1) - dis(l));
// 可以从左儿子转移而来,也可以从右儿子跨越中间而来。
rx[x] = max(rx[rc[x]], rx[lc[x]] + dis(r) - dis(mid));
// 可以从右儿子转移而来,也可以从左儿子跨越中间而来。
mx[x] = max(max(mx[lc[x]], mx[rc[x]]), lx[rc[x]] + rx[lc[x]] + dis(mid + 1) - dis(mid));
// 可以从儿子结点转移而来,或者计算出跨越中心情况的答案。
// dis 的差值实质上是边权
}
#undef dis
答案即为 (max{mx})。
如何处理叶结点的值?显然不能爆算子树中所有结点的距离,因为深度总和会达到 (O(n^2)) 级别。
对于一个 线段树上 的叶子结点 (x),其对应的 原树 结点为 (u)。对于其 父结点或重儿子,由于在同一条链上 ,无需过多考虑。(u) 的所有轻儿子,显然它们一定是其所在链的链顶。
对于其中一个轻儿子 (v),易知 (lx(root(v))) 子树 (v) 中向下延伸的最长合法路径。那么加上当前路径就是 一条“LCA 位于区间 ([dfn(u), dfn(u)])”的合法路径。((dfn(x)) 表示原树上结点 (x) 的 dfs 序)。
设 (d_1) 为一端为 (u) 的 最长向下 路径长,(d_2) 为 次长向下 路径长。不存在设为 (-infty)。
不难得出,(lx(x), rx(x)) 的值就是 (max(d_1, 0))。(mx(x)) 的值可以由最长、次长两条路径拼成。无需考虑路径会不会重合,因为来自不同的子树。由于白点自身可以作为路径的端点,(mx(x)) 的值需要分类讨论。
- 白点:(mx(x) = max(d_1, d_1 + d_2, 0))。
- 黑点:(mx(x) = max(d_1 + d_2, 0))。
对于最大值、次大值的维护,可以使用堆。在叶结点遍历轻儿子时顺便将堆更新。求次大值时只需将堆顶弹出,取值后重新塞回即可。
答案即为 (maxlimits_{xin ext{tops}} { mx(root(x))}),同样可以用一个全局堆维护。
建树操作参考代码:
void build(int& x, int l, int r) {
if (!x) x = ++total; // 动态开点
if (l == r) {
int u = pos[l];
getEdge(u, v) if (v->to != fa[u] && v->to != wson[u])
pt[u].insert(lx[root[v->to]] + dep[v->to] - dep[u]);
// pt 为堆
int d1 = pt[u].top(); // 最大
pt[u].erase(d1);
int d2 = pt[u].top(); // 次大
pt[u].insert(d1);
lx[x] = rx[x] = max(d1, 0);
mx[x] = max(d1, max(d1 + d2, 0));
return;
}
build(lc[x], l, mid);
build(rc[x], mid + 1, r);
pushup(x, l, r);
}
考虑修改操作。一个修改可能 会影响到其祖先的答案,于是我们需要一直向上跳。
设当前跳到的位置为 (x),上次位置的链顶为 (y)。
首先在 链顶父亲 结点的堆中删去 当前链顶的贡献,下一次跳在重新将 更新过的值插入。
那么在线段树上修改时,将堆中 (y) 方向轻儿子的贡献 重新插入 (x) 的堆中,像 build
一样维护即可。
同时别忘了更新全局堆。
下面给出修改的代码:
void update(int x, int l, int r, int u, int v) {
if (l == r) {
if (u != v)
pt[u].insert(lx[root[v]] + dep[v] - dep[u]);
int d1 = pt[u].top();
pt[u].erase(d1);
int d2 = pt[u].top();
pt[u].insert(d1);
if (color[u]) {
lx[x] = rx[x] = d1;
mx[x] = d1 + d2;
} else {
lx[x] = rx[x] = max(d1, 0);
mx[x] = max(d1, max(d1 + d2, 0));
}
return;
}
if (dfn[u] <= mid) update(lc[x], l, mid, u, v);
else update(rc[x], mid + 1, r, u, v);
pushup(x, l, r);
}
void change(int x) {
color[x] ^= 1;
if (color[x] == 0) ++white;
else --white;
for (int y = x; x; x = fa[x]) {
int top = wtop[x];
all.erase(mx[root[top]]);
if (fa[top]) pt[fa[top]].erase(lx[root[top]] + dep[top] - dep[fa[top]]);
update(root[top], dfn[top], dfn[top] + len[top] - 1, x, y);
all.insert(mx[root[top]]);
y = x = top;
}
}
那么算法基本算是完成了。
但实现非常复杂,细节多(上面代码)。
不过实测表现不差,原因是树剖、线段树的 (log) 都跑不满。
时间复杂度 (O(nlog^2 n))。
参考代码:https://vjudge.net/solution/26745510/8T7tgRJPSKwBPUqVL9r2