一 问题描述
有一棵 N 个节点的树,节点编号为 1~N ,每条边都有一个整数权值。每个节点都有一个颜色:白色或黑色。将 dist(a , b ) 定义为从节点 a 到节点 b 路径上的边权值之和。最初,所有节点都是白色的。执行以下两种操作:① C a ,修改节点 a 的颜色(从黑色到白色或从白色到黑色);② A 查询相距最远的两个
白色节点的距离 dist(a , b ),节点 a 和节点 b 都必须是白色的( a 可以等于 b )。显然,只要有一个白色节点,结果总是非负的。
二 输入和输出
1 输入说明
第 1 行包含一个整数 N (N ≤ 10^5 ),表示节点数。接下来的 N - 1 行,每行都包含三个整数 a、b、c ,表示在 a、b 之间有一条边,权值为 c(-1000 ≤ c ≤ 1000 )。在下一行包含一个整数 Q ( Q ≤ 10^5 ),表示指令数。接下来的 Q 行,每行都包含一条指令 C a 或 A。
2 输出说明
对每个指令 A 都单行输出结果。若树中没有白色节点,则输出“They have disappeared.”。
三 输入和输出样例
1 输入样例
3
1 2 1
1 3 1
7
A
C 1
A
C 2
A
C 3
A
2 输出样例
2
2
0
They have disappeared.
四 分析
本问题中的节点有黑白两种颜色,边权可能为负,包括变色和查询操作,因为节点可能变色,因此每次查询相距最远的两个白色节点的距离难度较大,可以采用边分治解决。
五 算法设计
1 重建树。
添加虚节点,使每个节点都不超过 2 度,将虚点的颜色定为黑色,虚边的权值为 0(对查询无影响)。
2 求解子树的大小并建立距离树。
T[rt]为边分治产生的树。记录树根 rt 的答案和一个优先队列(最大值优先,记录子树中白色节点到树根的距离)。
3 找中心边,删掉中心边,递归求解左右两棵子树。
4 求解 T[rt] 的 ans。
5 修改 u 节点的颜色。
重点说明一下第 4 步和第 5 步。
求解 T[rt] 的 ans 的过程:初始化 T[rt] 的 ans 为 -1,T[rt] 优先队列中的黑色节点出队。若 T[rt] 没有左右子节点,而且 T[rt] 的根为白色节点,则 rt 的 ans 为 0,否则 ans = max{左子树的 ans,右子树的 ans,左
子树的最远距离 + 右子树的最远距离 + 中心边长度}。
六 图解
一棵树如下图所示,初始时全部为白色节点,求解相距最远的两个白色节点之间的距离。
一棵树如下图所示,初始时全部为白色节点,求解相距最远的两个白色节点之间的距离。
1 重建树,添加虚节点,使每个节点都不超过 2 度,将虚节点的颜色定为黑色,虚边的权值为 0。
2 求解子树的大小并建立距离树。将子树中的所有白色节点及其到树根的距离都加入T[] 树根的优先队列中。重建树、距离树和 T[]树如下图所示。
3 求中心边,找到的中心边为 1-9,中心边的权值为 0,如下图所示。将中心边权值添加到根的 midlen,将中心边的两个端点 p1 、p2 作为左右子节点的根进行递归求解。
4 删掉中心边,递归求解左右两棵子树。
将子树中的所有白色节点及其到树根的距离都加入树根的优先队列中。
5 求解答案。
初始化树根 rt 的 ans 为 -1,然后弹出 rt 的优先队列中的黑色节点。若 rt 没有左右儿子,而且 rt 的根为白色节点,则 rt 的 ans 为 0;否则 ans = max{左子树的ans,右子树的ans,左子树的最远距离 + 右子树的最远距离 + 中心边长度},左右子树优先队列中的第 1 个 dis 就是白色节点到该子树树根的最远距离。
6 修改颜色。
将节点 u 的颜色取反,然后处理包含节点 u 的节点 v (距离树中节点 u 的邻接点),节点 u 若变色后为白色节点,则加入节点 v 的优先队列;最后更新节点 v 的 ans。节点 u 若变为黑色节点,则更新时会把该黑色节点出队,重新计算 ans。
例如,将3号节点变色,原来为白色,变色后为黑色。
更新距离树中3号节点的所有邻接点的 ans,e :1-9表示中心边为1-9。距离树如下图所示。
七 代码
package com.platform.modules.alg.alglib.spojqtree4;
import java.util.PriorityQueue;
public class Spojqtree4 {
private int maxn = 200005;
private int maxe = 4000005;
int Head[] = new int[maxn];
int head[] = new int[maxn];
int rear, tot;
int tail[] = new int[maxn];
int mark[] = new int[maxn];
int sz[] = new int[maxn];
int N, n, cnt, root, midedge, Max;
node T[] = new node[2 * maxn];
public String output = "";
// 原图
private Edge edge[] = new Edge[maxe];
// 重构图
private Edge E[] = new Edge[maxe];
public Spojqtree4() {
for (int i = 0; i < edge.length; i++) {
edge[i] = new Edge();
E[i] = new Edge();
}
for (int i = 0; i < T.length; i++) {
T[i] = new node();
}
}
// 原图初始化
void init() {
for (int i = 0; i < head.length; i++) {
head[i] = -1;
}
tot = 0;
}
// 重构图初始化
void INIT() {
for (int i = 0; i < Head.length; i++) {
Head[i] = -1;
}
rear = 0; // 下标从零开始
}
// 原图加边
void add(int u, int v, int w) {
edge[tot].v = v;
edge[tot].w = w;
edge[tot].nxt = head[u];
head[u] = tot++;
}
// 重构图加边
void ADD(int u, int v, int w) {
E[rear].v = v;
E[rear].w = w;
E[rear].nxt = Head[u];
Head[u] = rear++;
}
// 删除 u 结点的 i 号边
void Delete(int u, int i) {
if (Head[u] == i)
Head[u] = E[i].nxt;
else
E[E[i].pre].nxt = E[i].nxt; // 跳过该边
if (tail[u] == i) // 指向 u 结点的最后一条边,相当于尾指针
tail[u] = E[i].pre;
else
E[E[i].nxt].pre = E[i].pre; // 双向链表修改前驱
}
// 保证每个点的度不超过 2
void build(int u, int fa) {
int father = 0;
for (int i = head[u]; i != -1; i = edge[i].nxt) {
int v = edge[i].v, w = edge[i].w;
if (v == fa) continue;
if (father == 0) // 还没有增加子节点,直接连上
{
ADD(u, v, w);
ADD(v, u, w);
father = u;
} else // 已经有一个子节点,则创建一个新节点,把v连在新节点上
{
mark[++N] = 0;
ADD(N, father, 0);
ADD(father, N, 0);
father = N;
ADD(v, father, w);
ADD(father, v, w);
}
build(v, u);
}
}
// nxt 是下一条边的编号,pre 是上一条边的编号
void get_pre() // 得到每条边的前驱
{
for (int i = 0; i < tail.length; i++) {
tail[i] = -1;
}
for (int i = 1; i <= N; i++) {
for (int j = Head[i]; j != -1; j = E[j].nxt) {
E[j].pre = tail[i];
tail[i] = j; // 指向 u 结点的最后一条边,相当于尾指针
}
}
}
// 重构图
void rebuild() {
// 重构图初始化
INIT();
N = n;
for (int i = 1; i <= N; i++)
mark[i] = 1;
build(1, 0);
get_pre(); // 得到每条边的前驱
init(); // 原图初始化
}
// 求解每个子树大小
void dfs_size(int u, int fa, int dis) {
add(u, root, dis); // 添加每个点到 root 的距离到距离树
if (mark[u] == 1) // 如果是白点,则压入根节点 root 的队列,dis为 u 到根 root 的距离
T[root].q.add(new point(u, dis)); // 队列中保存白点及其到 root 的距离
sz[u] = 1;
for (int i = Head[u]; i != -1; i = E[i].nxt) {
int v = E[i].v, w = E[i].w;
if (v == fa) continue;
dfs_size(v, u, dis + w);
sz[u] += sz[v];
}
}
// 找中心边
void dfs_midedge(int u, int code) {
if (Math.max(sz[u], sz[T[root].rt] - sz[u]) < Max) {
Max = Math.max(sz[u], sz[T[root].rt] - sz[u]); // sz[T[root].rt]为该子树结点总数
midedge = code;
}
for (int i = Head[u]; i != -1; i = E[i].nxt) {
int v = E[i].v;
if (i != (code ^ 1))
dfs_midedge(v, i);
}
}
// 更新 T[id] 的 ans
void PushUP(int id) {
T[id].ans = -1; // 初始为 -1
while (!T[id].q.isEmpty() && mark[T[id].q.peek().u] == 0) // 弹出黑点
T[id].q.poll();
int ls = T[id].ls, rs = T[id].rs; // ls 为左儿子,rs 为右儿子
if (ls == 0 && rs == 0) // 根没有左右儿子
{
if (mark[T[id].rt] == 1) // 根为白结点
T[id].ans = 0;
} else {
if (T[ls].ans > T[id].ans) // 如果左儿子的结果大于根
T[id].ans = T[ls].ans;
if (T[rs].ans > T[id].ans) // 如果右儿子的结果大于根
T[id].ans = T[rs].ans;
if (!T[ls].q.isEmpty() && !T[rs].q.isEmpty()) // 穿过中心边的
T[id].ans = Math.max(T[id].ans, T[ls].q.peek().dis + T[rs].q.peek().dis + T[id].midlen);
}
}
// id:T树节点编号
void DFS(int id, int u) {
root = id;
Max = N;
midedge = -1;
T[id].rt = u;
dfs_size(u, 0, 0); // 求解每个子树大小
dfs_midedge(u, -1); // 找中心边
if (midedge != -1) {
// 中心边的左右2个端点
int p1 = E[midedge].v; // p1:v midedge: u->v
int p2 = E[midedge ^ 1].v; // p2:u
// 中心边长度
T[id].midlen = E[midedge].w;
// 左右子树
T[id].ls = ++cnt;
T[id].rs = ++cnt;
// 删除中心边
Delete(p1, midedge ^ 1); // 删除 p1 结点的 i 号边
Delete(p2, midedge);
DFS(T[id].ls, p1);
DFS(T[id].rs, p2);
}
PushUP(id); // 更新 rt 的ans
}
void update(int u) { // 改变 u 的颜色
mark[u] ^= 1; // 颜色取反
for (int i = head[u]; i != -1; i = edge[i].nxt) {
int v = edge[i].v, w = edge[i].w;
if (mark[u] == 1) // u 为白结点加入 v 结点队列
T[v].q.add(new point(u, w));
PushUP(v); // 更新 v
}
}
public String cal(String input) {
String[] line = input.split("\n");
n = Integer.parseInt(line[0]);
init();//原树初始化
int u, v, w;
for (int i = 1; i < n; i++) {
String[] num = line[i].split(" ");
u = Integer.parseInt(num[0]);
v = Integer.parseInt(num[1]);
w = Integer.parseInt(num[2]);
add(u, v, w);
add(v, u, w);
}
rebuild(); // 重建树
DFS(cnt = 1, 1); // 求解答案
int m, x;
m = Integer.parseInt(line[n]);
for (int i = 1; i <= m; i++) {
String query = line[n + i];
if (query.charAt(0) == 'A') { // 输出树中最远的两个白点距离
if (T[1].ans == -1)
output += "They have disappeared.\n";
else
output += T[1].ans + "\n";
} else {
String[] param = query.split(" ");
x = Integer.parseInt(param[1]);
update(x); // 改变 x 的颜色
}
}
return output;
}
}
class Edge {
int v, w, nxt, pre;
}
class point implements Comparable {
int u, dis;
point(int _u, int _dis) {
u = _u;
dis = _dis;
}
@Override
public int compareTo(java.lang.Object o) {
return this.dis > ((point) o).dis ? -1 : 1; // 降序
}
}
class node {
int rt; // 根节点编号
int midlen; // 中心边的权值
int ans; // 答案(最长树链)
int ls, rs; // 左右子树编号
PriorityQueue<point> q = new PriorityQueue<>(); // 优先队列
}