题目概述

对一棵树维护两种操作:翻转某个点的颜色,求 \(max\{ dist_{u, v} \}\) 且满足 \(u\) 的颜色和 \(v\) 的颜色都是白色( \(u,v\) 可以相同)。

思路

首先考虑若没有修改,给定带颜色的 \(N\) 个点怎么查询。

经典办法是树形 \(\text{dp}\) ,定义\(mx_u\) 表示在 \(u\) 的子树中从 \(u\) 出发距离最远的白点的距离,\(se_u\) 表示在 \(u\) 的子树中从 \(u\) 出发且不进入 \(mx_u\) 表示白点的子树距离最远的白点的距离,最后答案就是

\[\max_{ u \in [1, n] }\{ mx_u + se_u \}
\]

现在考虑动态维护这一个 \(\text{dp}\) ,由于修改结点 \(u\) 的颜色后更新 \(dp\) 状态是在从 \(u\) 到根结点的链上更新 \(\text{dp}\) 值,同时 \(\text{dp}\) 的更新可以写作线段树的合并区间类型,所以可以考虑使用树链剖分,这样对于从 \(u\) 更新到根结点最多只会经过 \(\log N\) 条重链,同时线段树上的单点修改,区间合并花费 \(O(\log N)\) ,可以在 \(O(N log^2 N)\) 的时间内完成此题。

做法

对于结点 \(u\) 记录 \(mx(u)\) 表示在 \(u\) 的子树中从 \(u\) 出发且不走中重子结点距离最远的白点的距离,\(se(u)\) 表示在 \(u\) 的子树中从 \(u\) 出发且不进入 \(mx(u)\) 表示白点的子树和 \(u\) 的重子结点距离最远的白点的距离,如果不存在,都记为 \(- \infty\) 。

概述

首先对树进行轻重链剖分。

因为每一条重链在 \(\text{dfs}\) 序上都是一段连续的区间,所以对于每一条重链开一棵线段树维护 \(\text{dfs}\) 序上的区间信息。

对于线段树上某一点维护区间 \([l, r]\) 定义

  • \(topdis\) :从 \(dfs\) 序为 \(l\) 的点,即当前区间所表示的链的顶端向其子树出发,不进入 \(dfs\) 序为 \(r\) 的重子结点的子树所能到达的最远的白点的距离
  • \(bottomdis\) :从 \(dfs\) 序为 \(r\) 的点,即当前区间所表示的链的底端出发,只在 \(dfs\) 序为 \(l\) 的子树中前进所能到达的最远的白点的距离
  • \(val\) :记 \(dis(u, v)\) 表示 \(u\) 到 \(v\) 的距离,原树中所有满足 \(dfn_{\text{LCA}(u, v)} \in [l, r]\) 的点对 \((u, v)\) 中最大的 \(dis(u, v)\)

因为要开多棵线段树,所以要动态开点,同时记录 \(rt_u\) 表示以 \(u\) 为 \(top\) 的重链的线段树的根结点编号。

现在假设我们已经维护好所有线段树(画大饼,展望未来),设 \(val_i\) 表示重链 \(i\) 的线段树的根结点的 \(val\)(\(i\) 为该重链的 \(top\)),那么答案就是 \(\max_{ i \in \{ u | \exists v, top_v = u \}} \{ val_i \}\) 。

树链剖分

维护

先来聊聊线段树中的区间合并,设当前区间为 \([l, r]\) ,结点为 \(id\),定义 \(dis(u, v)\) 表示 \(u\) 到 \(v\) 的距离,左儿子为 \(ls\) ,右儿子为 \(rs\),\(rnk_i\) 表示 \(dfs\) 序为 \(i\) 的结点编号,对于 \(topdis\) ,我们可以直接继承左儿子的 \(topdis\) ,也可以走过整段左儿子表示的链进入右儿子,并走右儿子的 \(topdis\) 。即

\[topdis_{id} \leftarrow max( topdis_{ls} , dis(rnk_l, rnk_{mid + 1}) + topdis_{rs})
\]

像这样



可走路径即图中的红色路径或粉色路径+绿色路径+蓝色路径,对应合并时取 \(max\) 的两个数。

\(bottomdis\)同理,即

\[bottomdis_{id} \leftarrow max( bottomdis_{rs} , dis(rnk_{mid}, rnk_r) + bottomdis_{ls})
\]

对于 \(val\) ,我们可以继承左右儿子的 \(val\) ,也可以从左儿子中的点走到右儿子中,即走左儿子的 \(bottomdis\) ,过 \((mid, mid + 1)\) ,走右儿子的 \(topdis\) ,转移即

\[val_{id} \leftarrow \max \{ val_{ls}, val_{rs}, bottomdis_{ls} + dis(rnk_{mid}, rnk_{mid + 1}) + topdis_{rs} \}
\]

对于线段树中的叶子结点 \(u\),可以利用 \(mx(u)\) 和 \(se(u)\) 更新。

因为 \(mx(u)\) 和 \(se(u)\) 的定义都不局限于当前重链,所以在更新当前重链前要把挂在该重链上的所有重链更新完,这一点乍一想很恶心,其实只需要对于 \(dfs\) 序反过来遍历并依次建树,因为对于挂在某条重链上的所有重链一定是在该重链遍历完再进行遍历(至少我的写法是这样)。

假设知道了 \(mx(u)\) 和 &se(u)& ,我们怎么更新 \(u\) 呢?我们分两种情况讨论:

  • 结点 \(rnk_u\) 为白色,\(topdis_u, bottomdis_u \leftarrow max(mx(rnk_u), 0)\) ,因为可以以 \(rnk_u\) 为起点和终点,所以与 \(0\) 取 \(max\)(后面就不解释了),\(val_u \leftarrow \max \{ mx(rnk_u), mx(rnk_u) + se(rnk_u), 0 \}\)
  • 结点 \(rnk_u\) 为黑色,\(topdis_u, bottomdis_u \leftarrow mx(rnk_u), val_u \leftarrow mx(rnk_u), mx(rnk_u) + se(rnk_u)\)

\(mx(rnk_u)\) 表示以 \(rnk_u\) 作为路径结尾的答案,\(mx(rnk_u) + se(rnk_u)\) 表示将以 \(rnk_u\) 为路径结尾的两条路径拼起来的答案。

现在考虑维护 \(mx(u)\) 和 \(se(u)\) ,由于我们会删除或加入白色点,所以用一个支持随机删除的堆来维护,这里我们可以偷懒使用\(\text{STL}\) 中的multiset,对每一个结点开一个堆,初始化时遍历 \(u\) 的轻儿子,用已更新好的重链来更新 \(u\) ,设 \(v\) 为 \(u\) 的轻儿子, \(id\) 为 \(v\) 所在重链的线段树的根结点,即插入 \(topdis_{id} + dis(u, v)\) 到 \(u\) 的堆中。

查询

对于每一条重链都会诞生一个答案,同时会实时修改,因为我们已经维护了一个支持随机删除和插入的堆,所以可以直接定义一个堆 \(ans\) 表示所有重链的答案的集合,查询时直接取出 \(ans\) 的堆顶元素即可。

修改

与树链剖分的板子相同,不过只有一个点 \(u\) ,所以单说跳的部分更简洁,但对于修改其实更加复杂。

首先,对于当前点 \(u\) ,它会影响到 \(fa_{top_u}\) 的 \(mx\) 和 \(se\) ,所以要在修改 \(u\) 前要消除对 \(fa_{top_u}\) 的影响,然后又要在修改后更新对于被撤销影响的位置的 \(mx\) 和 \(se\) ,即在 \(u\) 时删除 \(fa_{top_u}\) 的堆中的 \(topdis_{rt_{top_u}} + dis_{top_{u}} - dis_{fa_{top_{u}}}\) ,在 \(u\) 跳到 \(fa_{top_u}\) 后插入 \(topdis_{rt_{top_u}} + dis_{top_{u}} - dis_{fa_{top_{u}}}\) 。

然后,要修改 \(ans\) 这个堆,删除原本答案 \(val_{rt_{top_u}}\) ,修改后再插入新答案 \(val_{rt_{top_u}}\) 。


现在所有做法和细节就基本上讲完,最后还是要落到代码实现上,虽然说不算最长的那一类,但实现细节很多,建议理清思路后再打,不然盲目抄题解收获不大。

Code

/*
address:https://vjudge.net/problem/SPOJ-QTREE4
AC 2025/1/23 14:54
*/
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
const int INF = 0x3f3f3f3f;
int n, q;
struct edge {
int to, w;
};
vector<edge>G[N];
bool col[N];
struct Heap { // 支持随机删除堆
multiset<int, greater<int>>s;
inline void insert(int x) { s.insert(x); }
inline void erase(int x) {
auto it = s.lower_bound(x);
if (it != s.end()) s.erase(it);
}
inline int mx() { return s.empty() ? -INF : *s.begin(); }
inline int se() {
if (s.size() <= 1) return -INF;
auto it = s.begin();
it++;
return *it;
}
}a[N], ans;
/*
a[i].mx:在i的子树中离i最远的白点的距离
a[i].se:在i的子树中离i次远且与mx只在i相交的白点的距离
ans:每条重链的答案集合
*/
int siz[N], dis[N], dfn[N], rnk[N], top[N], fa[N], son[N], len[N]; // len[i]:重链i的长度
int rt[N], L[N], R[N]; // 每个重链的线段树的根节点和管辖区间
inline void dfs1(int u) {
siz[u] = 1;son[u] = 0;
for (auto e : G[u])
if (e.to != fa[u]) {
fa[e.to] = u;
dis[e.to] = dis[u] + e.w;
dfs1(e.to);
siz[u] += siz[e.to];
if (siz[son[u]] < siz[e.to]) son[u] = e.to;
}
}
int cntn;
inline void dfs2(int u) {
dfn[u] = ++cntn;
rnk[cntn] = u;
len[top[u]]++;
if (!son[u]) return;
top[son[u]] = top[u];
dfs2(son[u]);
for (auto e : G[u])
if (e.to != son[u] && e.to != fa[u]) {
top[e.to] = e.to;
dfs2(e.to);
}
}
int nodecnt;
#define ls (seg[id].lc)
#define rs (seg[id].rc)
#define mid (l + r >> 1)
struct Segment {
int lc, rc;
int topdis, bottomdis, val;
/*
topdis:离该重链顶部最远的白点的距离
bottomdis:离该重链底部最远的白点的距离
val:该重链的答案
*/
}seg[N << 2]; // 动态开点,对每条重链开一颗线段树
inline void merge(int id, int l, int r) {
seg[id].topdis = max(seg[ls].topdis, dis[rnk[mid + 1]] - dis[rnk[l]] + seg[rs].topdis);
// 左儿子的顶端,整段左儿子+右儿子顶端
seg[id].bottomdis = max(seg[rs].bottomdis, dis[rnk[r]] - dis[rnk[mid]] + seg[ls].bottomdis);
//同理
seg[id].val = max({ seg[ls].val, seg[rs].val, seg[ls].bottomdis + dis[rnk[mid + 1]] - dis[rnk[mid]] + seg[rs].topdis });
// 左儿子答案,右儿子答案,左儿子底端+中间的边+右儿子底端
}
inline void build(int id, int l, int r) {
if (l == r) {
int u = rnk[r];
for (auto e : G[u])
if (e.to != fa[u] && e.to != son[u]) a[u].insert(seg[rt[top[e.to]]].topdis + e.w); //从已更新完的重链转移,且两个区间不能相交,否则转移失效
int mx = a[u].mx(), se = a[u].se();
seg[id].topdis = seg[id].bottomdis = max(mx, 0); //初始所有点都是白点
seg[id].val = max({ mx, mx + se, 0 });
return;
}
ls = ++nodecnt;rs = ++nodecnt;
build(ls, l, mid);build(rs, mid + 1, r);
merge(id, l, r);
}
inline void change(int id, int l, int r, int x, int sontop) {
if (l == r) {
if (x != sontop) a[x].insert(seg[rt[sontop]].topdis + dis[sontop] - dis[x]); //更新被撤销的距离影响
int mx = a[x].mx(), se = a[x].se();
if (col[x]) { // 白点可以以自己为起点,与0取max
seg[id].topdis = seg[id].bottomdis = max(mx, 0);
seg[id].val = max({ 0, mx, mx + se });
}
else {
seg[id].topdis = seg[id].bottomdis = mx;
seg[id].val = mx + se;
}
return;
}
if (dfn[x] <= mid) change(ls, l, mid, x, sontop);
else change(rs, mid + 1, r, x, sontop);
merge(id, l, r);
}
inline void modify(int u) {
int sontop = u; // sontop:记录撤销对当前重链贡献的那条贡献
while (u != 0) {
ans.erase(seg[rt[top[u]]].val);
if (fa[top[u]]) a[fa[top[u]]].erase(seg[rt[top[u]]].topdis + dis[top[u]] - dis[fa[top[u]]]); //撤销对父亲重链距离的影响
change(rt[top[u]], L[top[u]], R[top[u]], u, sontop);
ans.insert(seg[rt[top[u]]].val);
sontop = top[u];
u = fa[top[u]];
}
}
inline void init() {
dfs1(1);
top[1] = 1;
dfs2(1);
for (int i = n;i >= 1;i--)
if (rnk[i] == top[rnk[i]]) {
rt[rnk[i]] = ++nodecnt;
L[rnk[i]] = i, R[rnk[i]] = i + len[rnk[i]] - 1;
build(rt[rnk[i]], i, i + len[rnk[i]] - 1);
ans.insert(seg[rt[rnk[i]]].val);
}
}
int main() {
scanf("%d", &n);
for (int i = 1;i < n;i++) {
int u, v, w;scanf("%d%d%d", &u, &v, &w);
G[u].push_back({ v, w });
G[v].push_back({ u, w });
}
init();
for (int i = 1;i <= n;i++) col[i] = true;
scanf("%d", &q);
int white = n;
while (q--) {
char op[2];scanf("%s", op);
if (op[0] == 'C') {
int u;scanf("%d", &u);
col[u] ^= 1;
white += col[u] ? 1 : -1;
modify(u);
}
else
if (white == 0) puts("They have disappeared.");
else printf("%d\n", ans.mx());
}
return 0;
}

总结

其实这道题用其他方法会更简单,比如动态分治一类,但这是我们训练树链剖分时做的题,所以就会有这样一个奇怪做法,但对思维和码力练习挺大的,好题++。

「SPOJ2666」QTREE4 - Query on a tree IV的更多相关文章

  1. SPOJ QTREE4 - Query on a tree IV

    You are given a tree (an acyclic undirected connected graph) with N nodes, and nodes numbered 1,2,3. ...

  2. 洛谷 P2056 [ZJOI2007]捉迷藏 || bzoj 1095: [ZJOI2007]Hide 捉迷藏 || 洛谷 P4115 Qtree4 || SP2666 QTREE4 - Query on a tree IV

    意识到一点:在进行点分治时,每一个点都会作为某一级重心出现,且任意一点只作为重心恰好一次.因此原树上任意一个节点都会出现在点分树上,且是恰好一次 https://www.cnblogs.com/zzq ...

  3. SP2666 QTREE4 - Query on a tree IV(LCT)

    题意翻译 你被给定一棵n个点的带边权的树(边权可以为负),点从1到n编号.每个点可能有两种颜色:黑或白.我们定义dist(a,b)为点a至点b路径上的权值之和. 一开始所有的点都是白色的. 要求作以下 ...

  4. SPOJ QTREE4 - Query on a tree IV 树分治

    题意: 给出一棵边带权的树,初始树上所有节点都是白色. 有两种操作: C x,改变节点x的颜色,即白变黑,黑变白 A,询问树中最远的两个白色节点的距离,这两个白色节点可以重合(此时距离为0). 分析: ...

  5. SPOJ QTREE4 Query on a tree IV ——动态点分治

    [题目分析] 同bzoj1095 然后WA掉了. 发现有负权边,只好把rmq的方式改掉. 然后T了. 需要进行底(ka)层(chang)优(shu)化. 然后还是T 下午又交就A了. [代码] #in ...

  6. SPOJ - QTREE4 Query on a tree IV 边分治

    题目传送门 题意:有一棵数,每个节点有颜色,黑色或者白色,树边有边权,现在有2个操作,1修改某个点的颜色, 2询问2个白点的之前的路径权值最大和是多少. 题解: 边分治思路. 1.重构图. 因为边分治 ...

  7. SPOJ QTREE4 SPOJ Query on a tree IV

    You are given a tree (an acyclic undirected connected graph) with N nodes, and nodes numbered 1,2,3. ...

  8. Query on a tree IV SPOJ - QTREE4

    https://vjudge.net/problem/SPOJ-QTREE4 点分就没有一道不卡常的? 卡常记录: 1.把multiset换成手写的带删除堆(套用pq)(作用很大) 2.把带删除堆里面 ...

  9. 「CF1039D」You Are Given a Tree

    传送门 Luogu 解题思路 整体二分. 的确是很难看出来,但是你可以发现输出的答案都是一些可以被看作是关键字处于 \([1, n]\) 的询问,而答案的范围又很显然是 \([0, n]\),这不就刚 ...

  10. 【SPOJ QTREE4】Query on a tree IV(树链剖分)

    Description 给出一棵边带权(\(c\))的节点数量为 \(n\) 的树,初始树上所有节点都是白色.有两种操作: C x,改变节点 \(x\) 的颜色,即白变黑,黑变白. A,询问树中最远的 ...

随机推荐

  1. Nuxt.js 应用中的 vite:serverCreated 事件钩子

    title: Nuxt.js 应用中的 vite:serverCreated 事件钩子 date: 2024/11/18 updated: 2024/11/18 author: cmdragon ex ...

  2. NASA的食物计划

    NASA的食物计划 题目传送门 题目告诉我们要在体积和重量都不超过的情况下输出最大卡路里,稍微思考一下就可以发现这题是一道01背包的变形题(01背包不会的点这里). 并且01背包需要空间优化. 那我们 ...

  3. SQL注入手工注入portswigger labs练习

    目录 1 什么是SQL注入 2 QL注入会发生在哪些地方 3 QL注入的类型有哪些 4 QL注入点如何探测 5 QL注入的一般步骤 6 QL注入的防御 7 SQL注入前需要了解的 8 场训练 port ...

  4. 扩展 Tomcat Web 服务器的功能

    把服务器提升到新的高度 本教程是所有 Web 服务器或应用服务器管理员的必读指南.尽管 Apache Tomcat 的实现与其他 Web 服务器略有不同,但是本教程为许多高级管理任务提供了一种符合逻辑 ...

  5. Integrating JDBC with Hibernate

    One of the powerful things about Hibernate is that you do not typically need to manually write SQL: ...

  6. 运维工具之Netdata

    [导语]:Netdata 是一个开源.免费.预配置.高灵敏度的分布式实时监控系统. 简介 Netdata的分布式实时监视代理以零配置的方式,从系统.硬件.容器和应用程序收集数千个指标,它可以运行在所有 ...

  7. uniapp权限判断

    写法如下 // 检查是否有写入外部存储的权限 function writeExternalStoragePermission() { return new Promise((resolve, reje ...

  8. 多段区间的时间滑块slider实现方式

    多段区间的时间滑块slider实现方式 写在前面:今天要实现一个尖峰平谷的数据配置,这可一下难倒我了,但是还好互联网上大神云集,感谢各位大神的倾情分享,现在就写下我的感悟,留给看到这篇文章的你 参考链 ...

  9. 【Amadeus原创】华为一键强制关闭后台应用的终极解决方法

    华为手机速度是快,用起来很顺手,但是最让人头疼的,就是紧急情况开会,我音乐关不了. 上划把全部应用删掉,也没用. 经过一阵子摸索,发现终极办法: 1, 打开华为自带的 手机助手- 应用启动管理 2, ...

  10. 中电金信:金Gien乐道 | 6月热门新闻盘点 回顾这一月的焦点事件