「SPOJ2666」QTREE4 - Query on a tree IV

news/2025/1/23 17:43:44/文章来源:https://www.cnblogs.com/keysky/p/18688268

题目概述

对一棵树维护两种操作:翻转某个点的颜色,求 \(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}) \]

像这样
合并topdis
可走路径即图中的红色路径或粉色路径+绿色路径+蓝色路径,对应合并时取 \(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取maxseg[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);}elseif (white == 0) puts("They have disappeared.");else printf("%d\n", ans.mx());}return 0;
}

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/873779.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

K8S静态PVPVC配置

持久卷与动态存储 NFS-PVC-PV实战案例1、创建后端存储NFS节点 [root@web04 ~]# mkdir -pv /kubelet/pv/pv00{1..3} mkdir: created directory ‘/kubelet/pv’ mkdir: created directory ‘/kubelet/pv/pv001’ mkdir: created directory ‘/kubelet/pv/pv002’ mkdir: created…

『Python底层原理』--CPython 虚拟机

在 Python 编程的世界里,我们每天都在使用 python 命令运行程序,但你是否曾好奇这背后究竟发生了什么? 本文将初步探究 CPython(Python 中最流行的实现)的一些内部机制,为了更好的来理解 Python 语言的底层运作。 1. CPython 简介 CPython 是用** C 语言**编写的 Python …

销售转化关键:掌握真诚沟通技巧

在日常沟通中,我特别不喜欢别人一上来就问“在吗”,或者只是单纯地发一个“微笑”表情。这种开场方式,总觉得少了点诚意和温度。 如果真的有事需要找人帮忙,与其冷冰冰地问“在吗”,不如先亲切地喊出对方的名字或昵称,然后再直截了当地说明来意。这样不仅能让对方感受到你…

万不得已,千万不要去外包

之前的失业日志系列里有讲,现在的就业环境不太好,有面试机会的基本都是外包岗位。刚好之前公司和外包有几次合作,主要是把一部分或者整个项目外包的外包公司,我们技术人员负责一部分的技术支持,通过几次的合作,对外包的工作也有可一个大概的了解,想谈一下自己对外包的了…

杂项-在vscdoe上使用jupyter notebook写javascript

目录准备安装notebook对javascript的支持进一步优化支持es6安装npm包将notebook集成到vsc中去 准备python / Anaconda :推荐专业的Python开发人员使用Anaconda进行包管理和版本控制安装jupyter notebook。pip3 install jupyter notebook 正常下载之后选择一个工作目录执行jupyt…

字节 GUI 代理模型 UI-TARS:具备像人一样的感知、推理、行动能力;SLAM-Omni:支持可控音色的语音对话模型

开发者朋友们大家好:这里是 「RTE 开发者日报」 ,每天和大家一起看新闻、聊八卦。我们的社区编辑团队会整理分享 RTE(Real-Time Engagement) 领域内「有话题的 新闻 」、「有态度的 观点 」、「有意思的 数据 」、「有思考的 文章 」、「有看点的 会议 」,但内容仅代表编辑…

htb Pandora walkthrough snmp + ssh 隧道 + 环境变量劫持

80端口web界面划到最下面发现两个邮箱 记录一下尝试加上域名dirsearch 扫描只扫到一个assets目录但是值得注意的是这个目录里面有个blog目录 也就意味着他的网站是有博客网站的 只是我们没扫描出来扫扫udp端口发现161 的 snmp是开着的用nmap 枚举进程 nmap -sU -p161 --script …

IT-Tools-开源好用的IT工具集

访问地址: 线上访问:https://it-tools.tech/ 开源社区:https://github.com/CorentinTh/it-tools 支持私有化部署: docker run -d --name it-tools --restart unless-stopped -p 8080:80 ghcr.io/corentinth/it-tools:latest简要介绍: IT-Tools 是为开发人员提供的在线工具…

4.4.2 版本更新来了!全新内容抢鲜看

产品更新概览 功能新增: 新版API支持在进行数据解析选择JSON节点时,全选下一层节点。 功能修复: 修复新版API认证功能; 修复报表后台系统用户权限问题。 功能优化: 支持在Linux X64版本上连接神通数据库; 支持连接2.x版本的TDengine数据库。 功能新增 在山海鲸可视化4.4.…

分享给大家一款网站四合一缩略图生成在线工具

​ 今天,我们要介绍一款能够极大提升网页设计和测试效率的工具——3M万能在线工具箱中的“四合一网页设备缩略图”生成工具。 为什么需要四合一缩略图生成在线工具? 在网页设计和开发过程中,确保网页在不同设备上(如台式机、笔记本电脑、平板电脑和手机)都能完美显示是至关…

ps ef命令查询进程号pid

ps ef命令查询进程号pid楼兰胡杨已经在《五分钟扫盲:25个工作中常用的Linux命令》分享了ps命令的简单使用方法,但是,写的过于笼统,这里详细介绍一下。语法:ps -ef | grep processName功能:查看当前进程 (process) 的状态。options:-e 显示所有进程,-f 全格式。说明:英…

详细剖析Java动态线程池的扩容以及缩容操作

前言 在项目中,我们经常会使用到线程来处理加快我们的任务。但为了节约资源,大多数程序员都会把线程进行池化,使用线程池来更好的支持我们的业务。 Java线程池ThreadPoolExecutor有几个比较核心的参数,如corePoolSize、maximumPoolSize等等。无论是在工作中还是在面试中,都…