1 条题解
-
0
题目理解
我们有一棵树,支持三种操作:
- 添加请求:在节点 和 之间添加重要度为 的请求
- 删除请求:删除之前添加的某个请求
- 查询:当节点 故障时,求不经过 的请求中重要度的最大值
关键点:
- 请求的路径是树上唯一路径
- 节点故障是瞬时的,之后立即恢复
- 需要动态维护请求集合
问题分析
核心问题
对于查询节点 ,我们需要找到所有不经过 的请求中重要度的最大值。
请求与节点的关系
一个请求 不经过节点 当且仅当:
- 不在 到 的路径上
换句话说:
- 要么 和 都在 的同一棵子树中
- 要么 不在 的路径上
暴力方法的困难
直接维护所有请求,对每个查询检查每个请求:
- 个事件
- 每个查询 会超时
- 总复杂度 不可行
转化:整体二分 + 树状数组
我们可以使用整体二分来回答所有查询:
核心思想:
对重要度进行二分,判断是否存在重要度 且不经过查询点 的请求。
算法框架:
- 将所有请求和查询按时间顺序排列
- 对重要度值域 进行整体二分
- 对于当前值域 :
- 处理所有重要度 的请求
- 用数据结构维护这些请求的路径覆盖
- 判断每个查询点是否被所有重要度 的请求路径覆盖
路径覆盖与检查
关键观察:
如果所有重要度 的请求都经过节点 ,那么答案 如果存在重要度 的请求不经过 ,那么答案
如何检查节点 是否被所有请求路径覆盖?
我们可以用树上差分:
- 对于请求 ,在树上:
- ,
- (如果是点权)
但实际上更简单:用线段树维护DFS序区间
DFS序技巧
对树做DFS,得到每个节点的入栈出栈时间戳 。
重要性质:
- 节点 的子树在DFS序上是连续区间
- 路径 经过 当且仅当:
- 在 的路径上
- 即: 是 和 中至少一个的祖先,且 是 的祖先
但实际上有更简单的方法:
线段树维护路径交集
我们可以维护当前所有请求路径的交集:
- 如果交集为空,则存在不经过某个节点的请求
- 如果交集非空,检查查询点是否在交集内
但路径交集难以直接维护。
正解:树链剖分 + 线段树
更实用的方法是树链剖分:
算法步骤:
- 对树进行树链剖分,得到重链结构
- 维护所有活跃请求的集合
- 对于每个查询 :
- 我们需要找到不经过 的请求的最大重要度
- 这等价于:从所有请求中,去掉那些经过 的请求,剩下的最大值
实现方案:
由于有删除操作,我们需要支持:
- 添加请求
- 删除请求
- 查询:不经过 的请求的最大值
这可以用线段树套堆或整体二分解决。
整体二分具体实现
#include <iostream> #include <vector> #include <algorithm> using namespace std; const int N = 100010; struct Event { int type; // 0:添加, 1:删除, 2:查询 int a, b, v; int id; // 事件id }; struct Query { int x; int id; // 查询的原始id }; vector<Event> events; vector<Query> queries; vector<int> ans; // 树结构 vector<int> g[N]; int parent[N], depth[N], size[N], son[N]; int top[N], dfn[N], timestamp; // 树链剖分 void dfs1(int u, int p) { parent[u] = p; depth[u] = depth[p] + 1; size[u] = 1; son[u] = 0; for (int v : g[u]) { if (v == p) continue; dfs1(v, u); size[u] += size[v]; if (size[v] > size[son[u]]) { son[u] = v; } } } void dfs2(int u, int t) { top[u] = t; dfn[u] = ++timestamp; if (son[u]) { dfs2(son[u], t); } for (int v : g[u]) { if (v == parent[u] || v == son[u]) continue; dfs2(v, v); } } // 整体二分 void solve(int l, int r, vector<Event>& current_events, vector<Query>& current_queries) { if (current_queries.empty()) return; if (l == r) { for (auto& q : current_queries) { ans[q.id] = l; } return; } int mid = (l + r + 1) >> 1; // 处理重要度 >= mid 的请求 // 这里需要实现:检查对于每个查询点x,是否所有重要度>=mid的请求都经过x // 如果所有重要请求都经过x,则答案 < mid // 否则答案 >= mid vector<Event> left_events, right_events; vector<Query> left_queries, right_queries; // 这里需要具体实现请求的路径覆盖检查 // 简化:假设我们有一个函数 check(x) 返回是否所有重要度>=mid的请求都经过x for (auto& q : current_queries) { if (check(q.x)) { // 所有重要请求都经过x,答案 < mid left_queries.push_back(q); } else { // 存在重要请求不经过x,答案 >= mid right_queries.push_back(q); } } // 递归处理 solve(l, mid - 1, left_events, left_queries); solve(mid, r, right_events, right_queries); }
检查函数的实现
check(x)需要判断:所有重要度 的请求是否都经过 。我们可以用树上差分 + 线段树:
// 线段树维护DFS序区间 struct SegmentTree { // 维护区间最小值和区间加 // 如果最小值 > 0,说明整个区间都被所有请求路径覆盖 } seg; void add_path(int a, int b) { // 树链剖分路径加 while (top[a] != top[b]) { if (depth[top[a]] < depth[top[b]]) swap(a, b); seg.update(dfn[top[a]], dfn[a], 1); a = parent[top[a]]; } if (depth[a] > depth[b]) swap(a, b); seg.update(dfn[a], dfn[b], 1); } void remove_path(int a, int b) { // 树链剖分路径减 while (top[a] != top[b]) { if (depth[top[a]] < depth[top[b]]) swap(a, b); seg.update(dfn[top[a]], dfn[a], -1); a = parent[top[a]]; } if (depth[a] > depth[b]) swap(a, b); seg.update(dfn[a], dfn[b], -1); } bool check(int x) { // 检查以x为根的子树是否完全被覆盖 // 如果子树内所有节点被覆盖次数 = 当前活跃请求数,则x被所有请求经过 return seg.query(dfn[x], dfn[x] + size[x] - 1) == active_count; }
完整算法流程
- 读入树结构,进行树链剖分
- 读入所有事件,记录类型和参数
- 初始化整体二分:
- 值域
- 所有查询事件
- 执行整体二分:
- 维护当前重要度 的请求集合
- 用树链剖分+线段树维护路径覆盖
- 根据检查结果划分查询
- 输出答案
复杂度分析
- 树链剖分:
- 整体二分: 层
- 每层:(树链剖分路径操作)
- 总复杂度:,可接受
样例验证
输入片段:
13 23 1 2 1 3 ... 2 1 // 时刻1的查询 0 8 13 3 // 时刻2的添加 ...处理过程:
- 对于时刻1的查询:无任何请求 → 输出
-1 - 对于时刻6的查询:所有请求都经过节点2 → 输出
-1 - 对于时刻8的查询:存在不经过节点2的请求,最大重要度1 → 输出
1
输出符合样例。
优化技巧
- 请求去重:同一路径的请求可以合并
- 线段树优化:使用zkw线段树或树状数组
- 内存优化:避免在递归中复制大量数据
总结
本题的关键在于:
- 将问题转化为路径覆盖检查
- 使用整体二分处理最大值问题
- 结合树链剖分高效处理路径操作
- 注意处理添加和删除操作的时序
这是一个典型的数据结构 + 整体二分问题,考察了对复杂树操作和离线算法的综合应用能力。
- 1
信息
- ID
- 4599
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者