1 条题解
-
0
好的,这道题是 NOI 2013 的「快餐店」问题,是一道经典的 基环树(环套树) 上的最优化问题。我将为你提供完整的解题思路与代码实现。
题目重述
我们有一个 个点 条边的 连通无向图(即基环树),快餐店可以设在 任意一个点 或 任意一条边的任意位置(不一定是端点)。
目标是:
找到一个位置,使得 距离该位置最远的顾客 的距离 最小,并输出这个最小距离。换句话说,就是找到基环树的 最小直径 的“中心”。
问题分析
1. 基环树结构
- 给定 个点 条边的连通图,必定有且仅有一个环。
- 去掉环上的边,剩下的每个连通块都是一棵树(称为“外向树”)。
2. 问题转化
设快餐店的位置为 ,定义 为所有点到 的最远距离。
我们要找 ,其中 可以是任意点或任意边上的任意位置。重要性质:
对于一棵树,如果只能设在点上,最优解是 树的中心(直径的中点),此时最远距离 = 。
如果允许设在边上,那么最优解可以设在直径路径的 中点(不一定是端点),此时最远距离 = 。对于基环树,最优解可能在:
- 某棵外向树内部(不经过环)
- 环上某个点
- 环上某条边的中间
核心思路
步骤 1:找环
使用 DFS 或拓扑排序找环,这里推荐用拓扑排序(不断删去度数为 1 的点)得到环上的点。
步骤 2:预处理每棵外向树
对每个环上的点 ,将其作为根,向外遍历其子树(不经过环上的其他点),计算:
- :从 出发,在其子树中能走到的最远距离(即该外向树的深度)。
- 同时求出该子树的直径,更新全局答案的候选。
步骤 3:环上问题转化为序列问题
将环展开成链 ( 为环上点数),并记录环上相邻点之间的边权 (连接 与 , 连接 与 )。
定义:
- :环上点 的外向树深度
- :从 按顺时针方向走到 的环上距离前缀和()。
如果快餐店设在环上,那么最远距离可能来自:
- 某棵外向树内部(已经在外向树内部计算过)
- 通过环走到另一棵外向树的距离
对于环上两个点 和 ,它们之间的环上距离有两种:
- 顺时针:
- 逆时针: 其中 是环的总长度。
对于快餐店设在环上某处(点或边),最远距离可以表示为: [ \max_{i} \left( d[i] + \text{环上某点到 } i \text{ 的最短距离} \right) ] 我们要最小化这个最大值。
关键优化
这是一个环形问题,可以破环为链,复制一倍。
定义: [ \text{dist}(i,j) = \min(S[j] - S[i],\ totalLen - (S[j] - S[i])) ] 但直接枚举是 的,需要优化。
重要观察:
如果快餐店设在环上某处,那么对于环上每个点 ,最远距离是: [ \max_{j} \left( d[j] + \text{dist}(i,j) \right) ] 我们要找 (可以是点或边的中间)使得这个最大值最小。破环为链技巧:
将环展开成 ,其中 ,。
那么对于任意 ,环上 到 的顺时针距离是 ,逆时针距离是 。设快餐店位置在 和 之间的边上某处(包括端点),那么对于环上点 ,最短距离是: [ \min(S[j] - S[i],\ totalLen - (S[j] - S[i])) ] 更一般地,如果设在 顺时针 距离处(),那么到 的顺时针距离是 ,逆时针类似。
但是,我们可以发现一个性质:最优位置一定在环上某条边的中点或端点上。因此,只需枚举边和点即可。
经典解法
更简洁的思路:
-
如果最优解在某棵外向树内部
那么答案就是该树的直径除以 2。我们可以在预处理时求出所有外向树的直径,取最大值 。 -
如果最优解在环上
考虑破环为链,复制一倍。
对于环上两个点 和 ,它们之间走环的最短路径不超过环长的一半。
因此,我们可以考虑环上顺序,用单调队列优化求: [ \max_{j} \left( d[j] + S[j] \right) \quad \text{和} \quad \max_{j} \left( d[j] - S[j] \right) ] 然后枚举断开环的位置,计算该情况下的直径,取最小值。
最终答案 = 吗?
不对,因为这是两个不同的情况,我们要取的是所有可能的最小值,所以: [ \text{答案} = \min\left( \frac{\text{外向树最大直径}}{2},\ \text{环上最优解} \right) ] 但是,环上最优解也可能比外向树内部直径的一半更小,所以要综合比较。实际上,最终答案是: [ \max\left( \text{所有外向树的最大直径},\ \text{环上最优解直径} \right) \div 2 ] 因为快餐店可以设在边的中间,所以直径可以折半。
更准确地说:
- 如果最优解在外向树内部,答案 = 该树的直径 / 2
- 如果最优解在环上,答案 = 环上最优解直径 / 2
取两者较小值?不,我们要找全局最小化最远距离,所以是取所有情况的最小值。
标准解法流程
- 找环(拓扑排序)
- 对每个环上点 DFS 求外向树深度 和该子树的直径
- 破环为链,复制一倍,求前缀和
- 对于每个 (),考虑断开环上 到 的边,此时基环树变成一棵树,其直径可能为:
- 某棵外向树自身的直径
- 环上两点 通过链连接的距离:()
- 对于固定的 , 必须位于断开后的同一侧,即 。
此时求 $d[u] + d[v] + S[v] - S[u] = (d[u] - S[u]) + (d[v] + S[v])$ 的最大值。
可以用单调队列维护 的最大值,枚举 更新答案。 - 对所有断开位置 求得的直径取最小值,然后除以 2,即为环上最优解。
- 最终答案 =
代码实现(C++)
#include <bits/stdc++.h> using namespace std; typedef long long LL; const int N = 1e5 + 5; const LL INF = 1e18; int n; vector<pair<int, LL>> g[N]; int deg[N]; bool on_cycle[N]; vector<int> cycle; LL d[N], tree_dia = 0; LL S[N * 2], A[N * 2]; void find_cycle() { queue<int> q; for (int i = 1; i <= n; i++) { deg[i] = g[i].size(); if (deg[i] == 1) q.push(i); } while (!q.empty()) { int u = q.front(); q.pop(); for (auto &e : g[u]) { int v = e.first; if (--deg[v] == 1) q.push(v); } } // 现在 deg > 1 的点都在环上 int start = -1; for (int i = 1; i <= n; i++) { on_cycle[i] = (deg[i] > 1); if (on_cycle[i]) start = i; } // 提取环的顺序 cycle.clear(); int cur = start, prev = -1; do { cycle.push_back(cur); int nxt = -1; for (auto &e : g[cur]) { int v = e.first; if (on_cycle[v] && v != prev) { nxt = v; break; } } prev = cur; cur = nxt; } while (cur != start); } LL dfs_depth(int u, int p) { LL max1 = 0, max2 = 0; for (auto &e : g[u]) { int v = e.first; LL w = e.second; if (v == p || on_cycle[v]) continue; LL dis = dfs_depth(v, u) + w; if (dis > max1) { max2 = max1; max1 = dis; } else if (dis > max2) { max2 = dis; } } tree_dia = max(tree_dia, max1 + max2); return max1; } int main() { scanf("%d", &n); for (int i = 0; i < n; i++) { int a, b; LL l; scanf("%d%d%lld", &a, &b, &l); g[a].push_back({b, l}); g[b].push_back({a, l}); } find_cycle(); int m = cycle.size(); // 计算环上每个点的外向树深度 for (int i = 0; i < m; i++) { int u = cycle[i]; d[u] = dfs_depth(u, -1); } // 计算环上边权 vector<LL> edge_len(m); for (int i = 0; i < m; i++) { int u = cycle[i]; int v = cycle[(i + 1) % m]; for (auto &e : g[u]) { if (e.first == v) { edge_len[i] = e.second; break; } } } // 破环为链,复制一倍 for (int i = 0; i < m * 2; i++) { int idx = i % m; A[i + 1] = d[cycle[idx]]; if (i > 0) { S[i + 1] = S[i] + edge_len[(i - 1) % m]; } } // 单调队列优化求环上最优解 LL ans_cycle = INF; deque<int> dq; for (int i = 1; i <= m * 2; i++) { while (!dq.empty() && dq.front() <= i - m) dq.pop_front(); if (!dq.empty()) { int j = dq.front(); ans_cycle = min(ans_cycle, A[i] + A[j] + S[i] - S[j]); } while (!dq.empty() && A[dq.back()] - S[dq.back()] <= A[i] - S[i]) { dq.pop_back(); } dq.push_back(i); } // 最终答案 LL ans = max(tree_dia, ans_cycle); printf("%.1lf\n", ans / 2.0); return 0; }
算法复杂度
- 找环:
- 求外向树深度:
- 单调队列处理环:, 为环上点数
- 总复杂度:,可以通过 的数据
总结
这道题考察了 基环树的处理技巧、破环为链、单调队列优化 以及 树的直径性质,是一道综合性很强的图论题目。核心在于将环上问题转化为序列问题,并用单调队列高效求解。
- 1
信息
- ID
- 6137
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者