1 条题解
-
0
「RMI 2018」Traffickers 题解
有 个城市的树,初始有 个贩运者,每个贩运者在 之间周期性移动:
- 从时间 开始,从 沿最短路径到
- 每个整数时间点在当前位置交付 1 单位货物
- 到达 后,下一个时间点瞬间传送到 ,继续循环
有三种操作:
- 添加贩运者
- 删除贩运者 (保证存在)
- 查询:在时间 内,所有贩运者在路径 上交付的货物总数
关键性质分析
1. 贩运者运动规律
设贩运者在 之间移动,树上的距离为 。
- 周期
- 在时间 ,位置由 决定:
- 如果 :在从 到 的路上,距离 为
- 如果 :在从 回 的路上,距离 为
2. 路径交集
对于贩运者路径 和查询路径 ,它们的交集 是树上的一个路径(可能为空)。
由于树结构,两条路径的交集可以高效计算:
- 找到 和 的公共部分
- 交集要么为空,要么是一条连续路径
3. 时间区间内的计数
我们需要计算:在时间 内,贩运者在交集 上的时间点数量。
设交集 在贩运者路径上的位置:从距离 为 到 ()。
贩运者在时间 在 上当且仅当:
解决方案
第一步:树预处理
使用 LCA(最近公共祖先)预处理,支持:
- 快速计算两点距离
- 快速计算路径交集
class Tree { int n; vector<vector<int>> adj; vector<int> depth, parent[20]; // 倍增数组 void dfs(int u, int p) { parent[0][u] = p; for (int v : adj[u]) { if (v == p) continue; depth[v] = depth[u] + 1; dfs(v, u); } } public: Tree(int n) : n(n), adj(n+1), depth(n+1) { for (int i = 0; i < 20; i++) parent[i].resize(n+1); } void add_edge(int u, int v) { adj[u].push_back(v); adj[v].push_back(u); } void build() { depth[1] = 0; dfs(1, 0); for (int i = 1; i < 20; i++) { for (int u = 1; u <= n; u++) { parent[i][u] = parent[i-1][parent[i-1][u]]; } } } int lca(int u, int v) { if (depth[u] < depth[v]) swap(u, v); // 提升u到与v同一深度 for (int i = 19; i >= 0; i--) { if (depth[u] - (1 << i) >= depth[v]) { u = parent[i][u]; } } if (u == v) return u; // 同时提升 for (int i = 19; i >= 0; i--) { if (parent[i][u] != parent[i][v]) { u = parent[i][u]; v = parent[i][v]; } } return parent[0][u]; } int dist(int u, int v) { int w = lca(u, v); return depth[u] + depth[v] - 2 * depth[w]; } // 判断点x是否在路径u-v上 bool on_path(int u, int v, int x) { int w = lca(u, v); return (on_path_segment(u, w, x) || on_path_segment(v, w, x)); } // 获取路径u-v上的所有点 vector<int> get_path(int u, int v) { int w = lca(u, v); vector<int> path; // 从u到w while (u != w) { path.push_back(u); u = parent[0][u]; } path.push_back(w); // 从w到v(不包括w) vector<int> right; while (v != w) { right.push_back(v); v = parent[0][v]; } reverse(right.begin(), right.end()); path.insert(path.end(), right.begin(), right.end()); return path; } };第二步:计算路径交集
对于贩运者路径 和查询路径 ,我们需要找到它们的交集。
// 计算两条路径的交集在贩运者路径上的位置 pair<int, int> get_intersection_range(Tree &tree, int u_t, int v_t, int u_q, int v_q) { int d = tree.dist(u_t, v_t); // 获取贩运者路径上的所有点 vector<int> path_t = tree.get_path(u_t, v_t); // 找到交集端点 int a = -1, b = -1; for (int i = 0; i < path_t.size(); i++) { int node = path_t[i]; if (tree.on_path(u_q, v_q, node)) { if (a == -1) a = i; b = i; } } if (a == -1) return {-1, -1}; // 无交集 return {a, b}; }第三步:计算单个贩运者在时间区间内的贡献
// 计算在时间[t1, t2]内,满足 t mod T ∈ S 的整数t的数量 long long count_in_range(long long t1, long long t2, long long T, long long a, long long b) { if (a > b) return 0; // S = [a, b] ∪ [T-b, T-a] long long total = 0; // 函数:计算在[l, r]内满足条件的数量 auto count_segment = [&](long long l, long long r, long long L, long long R) -> long long { if (l > r || L > R) return 0; // 调整到模T的范围内 l = max(l, 0LL); r = min(r, t2); if (l > r) return 0; // 计算完整周期 long long first_complete = (l + T - 1) / T * T; long long last_complete = (r / T) * T; if (first_complete > last_complete) { // 没有完整周期,直接计算 long long cnt = 0; for (long long t = l; t <= r; t++) { long long r_mod = t % T; if (L <= r_mod && r_mod <= R) cnt++; } return cnt; } // 完整周期数 long long cycles = (last_complete - first_complete) / T + 1; // 每个周期中满足条件的数量 long long per_cycle = max(0LL, R - L + 1); total = cycles * per_cycle; // 加上左边不完整部分 for (long long t = l; t < first_complete; t++) { long long r_mod = t % T; if (L <= r_mod && r_mod <= R) total++; } // 加上右边不完整部分 for (long long t = last_complete + 1; t <= r; t++) { long long r_mod = t % T; if (L <= r_mod && r_mod <= R) total++; } return total; }; long long cnt1 = count_segment(t1, t2, a, b); long long cnt2 = count_segment(t1, t2, T - b, T - a); return cnt1 + cnt2; }第四步:数据结构优化
由于 ,,我们不能对每个查询都遍历所有贩运者。
关键观察:路径长度最多 20,所以周期 。
我们可以按周期分组:
class TraffickerSystem { int n; Tree tree; // 按周期分组存储贩运者 vector<map<pair<int, int>, int>> traffickers_by_period; public: TraffickerSystem(int n) : n(n), tree(n) { // 周期最大为 38(因为路径最多20个城市,距离最大19,周期=2*19=38) traffickers_by_period.resize(39); } void add_trafficker(int u, int v) { int d = tree.dist(u, v); int T = 2 * d; traffickers_by_period[T][{min(u, v), max(u, v)}]++; } void remove_trafficker(int u, int v) { int d = tree.dist(u, v); int T = 2 * d; auto key = make_pair(min(u, v), max(u, v)); traffickers_by_period[T][key]--; if (traffickers_by_period[T][key] == 0) { traffickers_by_period[T].erase(key); } } long long query(int u_q, int v_q, long long t1, long long t2) { long long total = 0; // 遍历所有可能的周期 for (int T = 2; T <= 38; T += 2) { // 周期都是偶数 for (auto &[pair_uv, count] : traffickers_by_period[T]) { int u_t = pair_uv.first, v_t = pair_uv.second; // 计算交集范围 auto [a, b] = get_intersection_range(tree, u_t, v_t, u_q, v_q); if (a == -1) continue; // 无交集 // 计算贡献 long long d = T / 2; // 原距离 long long contrib = count_in_range(t1, t2, T, a, b); total += contrib * count; } } return total; } };第五步:进一步优化查询
上面的查询仍然是 ,最坏情况下 不可行。
优化:对于每个查询路径 ,预处理它与所有可能贩运者路径的交集信息。
由于路径长度有限(≤20),我们可以:
- 将查询路径 上的所有点提取出来(最多20个)
- 对于每个贩运者,快速判断是否与 有交集
但更好的方法是使用树链剖分+线段树:
// 使用树链剖分将树映射到线段树上 class HeavyLightDecomposition { int n; Tree &tree; vector<int> parent, depth, heavy, head, pos; int cur_pos; int dfs_hld(int u) { int size = 1, max_subtree = 0; for (int v : tree.adj[u]) { if (v == parent[u]) continue; parent[v] = u; depth[v] = depth[u] + 1; int subtree_size = dfs_hld(v); size += subtree_size; if (subtree_size > max_subtree) { max_subtree = subtree_size; heavy[u] = v; } } return size; } void decompose(int u, int h) { head[u] = h; pos[u] = cur_pos++; if (heavy[u] != -1) { decompose(heavy[u], h); } for (int v : tree.adj[u]) { if (v != parent[u] && v != heavy[u]) { decompose(v, v); } } } public: HeavyLightDecomposition(int n, Tree &tree) : n(n), tree(tree), parent(n+1), depth(n+1), heavy(n+1, -1), head(n+1), pos(n+1), cur_pos(0) {} void build() { dfs_hld(1); decompose(1, 1); } // 查询路径u-v上的信息 template<typename F> void query_path(int u, int v, F &&process) { for (; head[u] != head[v]; v = parent[head[v]]) { if (depth[head[u]] > depth[head[v]]) swap(u, v); process(pos[head[v]], pos[v]); } if (depth[u] > depth[v]) swap(u, v); process(pos[u], pos[v]); } };第六步:完整算法框架
1. 读入N,构建树,预处理LCA和HLD 2. 读入K个初始贩运者,加入系统 3. 对于每个查询Q: if type == 1: 添加贩运者 if type == 2: 删除贩运者 if type == 3: 计算并输出答案第七步:复杂度分析
- 预处理:
- 添加/删除贩运者:
- 查询:需要更复杂的分析
由于路径长度限制(≤20),我们可以设计更高效的查询算法。一个可能的方法是:
- 对于每个贩运者 ,将其路径上的所有点存储
- 对于查询路径 ,也提取所有点
- 快速计算交集
但由于 和 都很大,可能需要离线处理或分块算法。
算法标签
- 树结构:LCA、树链剖分、树上路径
- 数据结构:线段树、树状数组、分块
- 数论:周期性分析、模运算
- 动态维护:添加/删除操作
- 区间查询:时间区间内的计数
总结
本题是一道树上路径+周期性运动+动态维护的综合难题:
- 核心:分析贩运者的周期性运动模式
- 关键:高效计算路径交集和时间区间内的计数
- 难点:在动态更新下处理大量查询
- 突破口:路径长度限制使得周期很小,可以按周期分组处理
对于满分需要设计 或更好的算法。在实际实现中,可能需要结合树链剖分、线段树维护区间信息,以及利用周期性进行数学优化。
- 1
信息
- ID
- 5890
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者