1 条题解
-
0
菜系餐厅最小交通费用问题 - 详细题解
问题分析
问题背景
- 交通网络是一棵 n 个节点的树(n-1 条边,无环且连通),每条边有票价(权重)
- 每个节点对应一家餐厅,属于 1~r 中的某类菜系
- 每个查询给出两个用户的位置(p, q)和目标菜系 s
- 需要找到菜系为 s 的餐厅 u,使得
dist(p,u) + dist(q,u)最小
关键难点
- 直接对每个查询遍历所有颜色为 s 的节点会超时(O(nq))
- 需要高效处理大量查询(q ≤ 100,000)
核心算法思路
1. 问题转化与数学推导
对于查询
(p, q, s),设目标餐厅为 u,则总费用为:cost = dist(p,u) + dist(q,u)利用树上距离的性质,可以改写为:
cost = dist(p,q) + 2 × dist(LCA(p,q), u)但这个公式只在 u 在 p-q 路径上时准确。
更通用的方法是多源最短路:以所有菜系 s 的餐厅为起点,计算到 p 和 q 的最短距离。
2. 整体算法框架
代码采用按颜色分组处理的策略:
-
预处理阶段:
- 树的DFS遍历,计算深度、距离、DFS序
- LCA倍增数组预处理
- 按颜色分组存储节点
-
查询处理阶段:
- 对每个颜色,构建包含所有相关节点的虚树
- 在虚树上运行多源最短路
- 回答所有涉及该颜色的查询
详细算法步骤
步骤1:树的预处理
void dfs(int x, int f) { st[x] = ++dc; // DFS进入时间戳 anc[x][0] = f; // 直接父节点 // 倍增数组预处理 for (int j = 1; j <= 20; j++) anc[x][j] = anc[anc[x][j-1]][j-1]; // 遍历子节点 for (auto [v, w] : g[x]) { if (v == f) continue; d[v] = d[x] + w; // 到根节点的距离 dep[v] = dep[x] + 1; // 深度 dfs(v, x); } ed[x] = dc; // DFS离开时间戳 }预处理内容:
st[x],ed[x]:DFS序,用于判断节点间的祖先关系d[x]:节点 x 到根节点的距离dep[x]:节点深度anc[x][k]:倍增数组,用于快速查询LCA
步骤2:LCA查询函数
int lca(int x, int y) { if (dep[x] < dep[y]) swap(x, y); // x 跳到与 y 同一深度 for (int i = 20; i >= 0; i--) if (dep[x] - (1 << i) >= dep[y]) x = anc[x][i]; if (x == y) return x; // 同时向上跳 for (int i = 20; i >= 0; i--) if (anc[x][i] != anc[y][i]) x = anc[x][i], y = anc[y][i]; return anc[x][0]; }步骤3:查询预处理
for (int i = 1; i <= q; i++) { int x, y, c; cin >> x >> y >> c; // 检查菜系是否存在 if (!src[c].size()) { ans[i] = -1; continue; } // 计算固定部分:x 到 y 的距离 ans[i] += d[x] + d[y] - 2 * d[lca(x, y)]; // 记录相关节点 key[c].push_back(x); key[c].push_back(y); qr[c].push_back({x, y, i}); }这里利用了树上距离公式:
dist(x,y) = d[x] + d[y] - 2 × d[lca(x,y)]步骤4:虚树构建(核心优化)
虚树只保留与当前颜色相关的节点及其LCA,大幅减少节点数量。
void SOLVE::solve(int C) { // 1. 收集关键节点 auto v = key[C]; // 颜色C的所有相关节点 // 2. 去重和排序 sort(v.begin(), v.end()); v.erase(unique(v.begin(), v.end()), v.end()); // 3. 加入所有LCA for (int i = 1; i < m; i++) if (!vis[x = lca(a[i], a[i+1])]) vis[a[++tot] = x] = 1; // 4. 按DFS序排序后构建虚树 sort(a + 1, a + m + 1, cmp); // 5. 用栈构建虚树结构 for (q[t = 1] = 1, i = 2; i <= m; q[++t] = a[i++]) { while (st[a[i]] < st[q[t]] || ed[a[i]] > ed[q[t]]) t--; if (q[t] != a[i]) add_edge(q[t], a[i]); } }虚树构建的关键:
- 只保留必要的节点(餐厅节点 + 查询节点 + 它们的LCA)
- 保持原树的祖先关系
- 边权为原树中对应路径的距离
步骤5:多源最短路计算
// 初始化距离 for (auto x : use) ds[x] = 1e18; // 所有颜色C的餐厅作为起点 for (auto x : src[C]) ds[x] = 0, q.push({ds[x], x}); // Dijkstra算法 while (!q.empty()) { auto [dis, x] = q.top(); q.pop(); if (dis != ds[x]) continue; for (auto [v, w] : e[x]) { if (ds[v] > ds[x] + w) { ds[v] = ds[x] + w; q.push({ds[v], v}); } } }步骤6:查询答案计算
// 预处理倍增最小值数组 for (auto x : use) mn[x][0] = ds[x]; for (int j = 1; j <= 20; j++) for (auto x : use) { anc[x][j] = anc[anc[x][j-1]][j-1]; mn[x][j] = min(mn[x][j-1], mn[anc[x][j-1]][j-1]); } // 计算路径最小值 for (auto [x, y, id] : qr[C]) ans[id] += calc(x, y) * 2;calc(x, y)函数计算 x 到 y 路径上的最小ds值,这对应着最优餐厅的选择。
算法复杂度分析
步骤 时间复杂度 说明 树预处理 O(n log n) DFS + 倍增数组 LCA查询 O(log n) per query 二进制跳跃 虚树构建 O(k log k) per color k为相关节点数 多源最短路 虚树上Dijkstra 总复杂度 O((n + q) log n) 可处理1e5级别数据
关键技术与学习要点
1. 虚树技术
- 目的:压缩树结构,只保留关键节点
- 应用场景:需要频繁在树的某个子集上操作时
- 构建方法:按DFS序排序,用栈维护当前链
2. 多源最短路
- 核心思想:多个起点到其他点的最短距离
- 实现方法:初始将所有起点距离设为0,入优先队列
- 在树上的优势:树结构保证最短路径唯一
3. 倍增法应用
- LCA查询
- 路径最小值查询
- 都需要 O(log n) 的预处理和查询时间
4. 分组处理思想
- 按颜色将查询分组
- 每个颜色独立处理,减少重复计算
- 适合颜色数较多但每个颜色的查询较集中的情况
样例验证
以样例第一天查询
(1, 3, 3)为例:- 颜色3的餐厅:节点3, 5
- 构建虚树包含:1, 3, 5 及相关LCA
- 多源最短路从节点3,5开始传播
- 计算得到最小费用:7
验证:
dist(1,3) + dist(3,3) = 7 + 0 = 7这个解法综合运用了多种树算法,是处理树上带约束最短路径问题的经典范例。
- 1
信息
- ID
- 4372
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者