题解:洛谷 P3199 最小圈
【题目来源】
【题目描述】
考虑带权有向图 \(G=(V,E)\) 以及 \(w:E\rightarrow \R\),每条边$ e=(i,j)\((\)i\neq j\(,\)i, j\in V$)的权值定义为 \(w_{i,j}\)。设 \(n=|V|\)。
\(c=(c_1,c_2,\cdots,c_k)\)(\(c_i\in V\))是 \(G\) 中的一个圈当且仅当 \((c_i,c_{i+1})\)(\(1\le i<k\))和 \((c_k,c_1)\) 都在 \(E\) 中。称 \(k\) 为圈 \(c\) 的长度,同时记 \(c_{k+1}=c_1\),并定义圈 \(c=(c_1,c_2,\cdots,c_k)\) 的平均值为
\(\mu(c)= \frac 1 k \sum\limits_{i=1}^{k} w_{c_i,c_{i+1}}\)
即 \(c\) 上所有边的权值的平均值。设 \(\mu'(G)=\min_c\mu(c)\) 为 \(G\) 中所有圈 \(c\) 的平均值的最小值。
给定图 \(G=(V,E)\) 以及 \(w:E\rightarrow \R\),求出 \(G\) 中所有圈 \(c\) 的平均值的最小值 \(\mu'(G)\)。
【输入】
第一行两个正整数,分别为 \(n\) 和 \(m\),并用一个空格隔开。其中 \(n=|V|\),\(m=|E|\) 分别表示图中有 \(n\) 个点 和 \(m\) 条边。
接下来 \(m\) 行,每行三个数 \(i,j,w_{i,j}\),表示有一条边 \((i,j)\) 且该边的权值为 \(w_{i,j}\),注意边权可以是实数。输入数据保证图 \(G=(V,E)\) 连通,存在圈且有一个点能到达其他所有点。
【输出】
一个实数 \(\mu'(G)\),要求精确到小数点后 \(8\) 位。
【输入样例】
4 5
1 2 5
2 3 5
3 1 5
2 4 3
4 1 3
【输出样例】
3.66666667
【核心思想】
-
问题分析:给定一个有向带权图,需要找出所有有向圈中边权平均值的最小值。直接枚举所有圈不可行,而该问题可通过二分答案转化为判定性问题:是否存在一个圈的平均值小于给定的阈值 x。利用“减去 x 后判定负环”的经典技巧,可将原优化问题转化为多次判负环问题。
-
算法选择:
- 二分答案:最小值 μ 的取值范围在边权范围内(通常设 \([-10^7, 10^7]\)),且具有单调性——若存在平均值 ≤ x 的圈,则任意更大的 x 也满足,因此可以二分搜索最小可行 x。
- 判负环(SPFA):对于每次猜测的 mid,将每条边的权值减去 mid,然后使用 SPFA 检测图中是否存在负环。若存在负环,则说明存在圈的平均值小于 mid,答案应在左半区间;否则在右半区间。
- DFS 版 SPFA:利用深度优先搜索检测负环,可提前终止,效率较高。
-
关键步骤:
- 读入与建图:读取 n, m 及每条有向边 \((u, v, w)\),使用链式前向星存储。
- 二分搜索:左边界
l = -1e7,右边界r = 1e7,精度控制到1e-10。- 每次取
mid = (l + r) / 2。 - 调用
check(mid):将每条边的权值临时改为w - mid,用 SPFA 判断图中是否存在负环。 - 若存在负环,则
r = mid;否则l = mid。
- 每次取
- 输出:循环结束后输出
r(保留 8 位小数)。
-
时间/空间复杂度:
- 时间复杂度:\(O(\log(\text{范围}/\text{精度}) \cdot (n \cdot m))\),二分约 60 次,每次 SPFA 最坏 \(O(nm)\),实际较快。
- 空间复杂度:\(O(n + m)\),存储图和辅助数组。
-
二分答案 + SPFA 判负环的核心思想:
- 问题转化:求最小平均值 \(\mu\) 等价于判定是否存在圈的平均值 < x。将边权 \(w\) 减去 \(x\) 后,圈的权值和变为 \(\sum (w - x) = \sum w - kx\),因此圈平均 < x 当且仅当新权值和为负。
- 负环检测:在有向图中,若存在负环,则 SPFA 会在环上反复松弛,可通过节点入队次数或 DFS 回溯标记判断。
- 二分单调性:随着 \(x\) 增大,边权整体减小,负环更容易出现,因此判定结果单调,适合二分。
- 适用场景:适用于求有向图中最小平均权值圈的问题,是二分答案与图论算法的经典结合。
【算法标签】
提高+ #SPFA
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int N = 3005; // 最大节点数
const int M = 10005; // 最大边数
int n, m; // n: 节点数, m: 边数
int vis[N]; // 访问标记数组
int h[N], ne[M], to[M], tot; // 链式前向星存储图
double d[N], w[M]; // d: 距离数组, w: 边权数组
// 链式前向星添加边
void add(int a, int b, int c)
{
to[++tot] = b; // 存储终点
ne[tot] = h[a]; // 存储下一条边
w[tot] = c; // 存储边权
h[a] = tot; // 更新头指针
}
// SPFA算法检测负环(带偏移量x)
bool spfa(int u, double x)
{
vis[u] = 1; // 标记当前节点已访问
for (int i = h[u]; i; i = ne[i]) // 遍历所有邻边
{
int v = to[i];
if (d[v] > d[u] + w[i] - x) // 松弛操作(带偏移量)
{
d[v] = d[u] + w[i] - x;
if (vis[v] || spfa(v, x)) // 如果已访问或发现负环
return true;
}
}
vis[u] = 0; // 回溯时取消标记
return false;
}
// 检查是否存在负环(带偏移量x)
bool check(double x)
{
memset(d, 0x3f, sizeof(d)); // 初始化距离为无穷大
memset(vis, 0, sizeof(vis)); // 清空访问标记
for (int i = 1; i <= n; i++) // 检查每个连通分量
if (spfa(i, x))
return true;
return false;
}
// 二分查找最小平均权值
double find()
{
double l = -1e7, r = 1e7; // 初始化二分范围
while (r - l > 1e-10) // 精度控制
{
double mid = (l + r) / 2;
if (check(mid)) // 如果存在负环
r = mid; // 尝试更小的平均值
else
l = mid; // 尝试更大的平均值
}
return r; // 返回最小平均权值
}
int main()
{
cin >> n >> m;
// 输入边信息并建图
for (int i = 1; i <= m; i++)
{
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
// 输出最小平均权值(保留8位小数)
printf("%.8f\n", find());
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 3005; // 最大节点数
const int M = 10005; // 最大边数
int n, m; // n: 节点数, m: 边数
int vis[N]; // 访问标记数组
int h[N], e[M], ne[M], idx=1; // 链式前向星存储图(idx从1开始)
int to[M]; // 边的终点数组(未使用)
double d[N], w[M]; // d: 距离数组, w: 边权数组
// 链式前向星添加边
void add(int a, int b, int c)
{
e[idx] = b; // 存储终点
w[idx] = c; // 存储边权
ne[idx] = h[a]; // 存储下一条边
h[a] = idx++; // 更新头指针
}
// SPFA算法检测负环(带偏移量x)
bool spfa(int u, double x)
{
vis[u] = 1; // 标记当前节点已访问
for (int i = h[u]; i; i = ne[i]) // 遍历所有邻边
{
int j = e[i];
if (d[j] > d[u] + w[i] - x) // 松弛操作(带偏移量)
{
d[j] = d[u] + w[i] - x;
if (vis[j] || spfa(j, x)) // 如果已访问或发现负环
return true;
}
}
vis[u] = 0; // 回溯时取消标记
return false;
}
// 检查是否存在负环(带偏移量x)
bool check(double x)
{
memset(d, 0x3f, sizeof(d)); // 初始化距离为无穷大
memset(vis, 0, sizeof(vis)); // 清空访问标记
for (int i = 1; i <= n; i++) // 检查每个连通分量
if (spfa(i, x))
return true;
return false;
}
// 二分查找最小平均权值
double find()
{
double l = -1e7, r = 1e7; // 初始化二分范围
double eps = 1e-10; // 精度控制
while (r - l > eps) // 精度控制循环
{
double mid = (l + r) / 2;
if (check(mid)) // 如果存在负环
r = mid; // 尝试更小的平均值
else
l = mid; // 尝试更大的平均值
}
return l; // 返回最小平均权值
}
int main()
{
cin >> n >> m;
// 输入边信息并建图
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
// 输出最小平均权值(保留8位小数)
printf("%.8f\n", find());
return 0;
}
【运行结果】
4 5
1 2 5
2 3 5
3 1 5
2 4 3
4 1 3
3.66666667
浙公网安备 33010602011771号