SPOJ-QTREE4 Query on a tree IV

Description

给出一棵边带权(\(c\))的节点数量为 \(n\) 的树,初始树上所有节点都是白色。有两种操作:

  • C x,改变节点 \(x\) 的颜色,即白变黑,黑变白。

  • A,询问树中最远的两个白色节点的距离,这两个白色节点可以重合(此时距离为 \(0\))。

\(q\) 次操作,输出所有查询的答案。

Hint

  • \(1\le n, q\le 10^5\)
  • \(0\le |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)\)

对于最大值、次大值的维护,可以使用堆。在叶结点遍历轻儿子时顺便将堆更新。求次大值时只需将堆顶弹出,取值后重新塞回即可。

答案即为 \(\max\limits_{x\in \text{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(n\log^2 n)\)

参考代码:https://vjudge.net/solution/26745510/8T7tgRJPSKwBPUqVL9r2

posted @ 2020-08-09 13:25  -Wallace-  阅读(329)  评论(0编辑  收藏  举报