【洛谷P3199】最小平均环(01分数规划 + DFS-SPFA判负环)
一、题目完整题意
1. 基础定义
给定一张带权有向图 $G=(V,E)$,每条有向边 $(i,j)$ 拥有权值 $w_{i,j}$。
- 环定义:序列 $c=(c_1,c_2,\dots,c_k)$,满足 $(c_i,c_{i+1})$、$(c_k,c_1)$ 均为图中存在的边,$k$ 为环包含的边数;
- 环平均值:$\displaystyle \mu(c)=\frac{\sum_{i=1}^k w_{c_i,c_{i+1}}}{k}$,即环上所有边权的算术平均值;
- 题目目标:求出图内所有环平均值的最小值 $\mu'(c)=\min\ \mu(c)$。
2. 输入输出规则
输入:
- 第一行两个整数 $n,m$,代表图中点数、有向边条数;
- 后续 $m$ 行,每行三个数 $i,j,w_{i,j}$,代表一条有向边 $i\to j$,边权为 $w$;
- 数据保证图连通、至少存在一个环,存在点可抵达其余所有点。
输出:
输出最小环平均值,严格保留小数点后8位。
3. 数据范围
$n \le 3000,\ m \le 10000,\ |w_{i,j}| \le 10^7$
4. 样例演示
输入:
2 2
1 2 2.9
2 1 3.1
图中仅存在一个环,两条边权和 $2.9+3.1=6$,环长 $k=2$,平均值 $6/2=3.0$;
程序输出:3.00000000
二、核心算法推导:01分数规划数学变形
1. 建立二分判定不等式
设我们二分猜测一个平均值 $mid$,我们需要判定:是否存在一个环,它的平均值 $\le mid$。
设该环有 $k$ 条边,根据平均值定义:
$$
\frac{\sum_{边\in 环} w}{k} \le mid
$$
由于环的边数 $k>0$,不等式两边同乘 $k$,不等号方向不变:
$$
\sum w - k\cdot mid \le 0
$$
等价变形:
$$
\sum_{边\in 环} \big(w - mid\big) \le 0
$$
2. 图模型转化:负环判定
对原图每一条边,构造新权值 $w' = w - mid$。
此时不等式含义:图中存在一个总权和 $\le 0$ 的环,分为两种情况:
- 存在负环(总权 $<0$):完全满足条件,$mid$ 可行;
- 存在零环(总权 $=0$):刚好取到等于 $mid$ 的环,$mid$ 同样可行。
综上,判定逻辑简化为:新权图中是否存在负环。
- 存在负环:当前 $mid$ 偏大,我们可以尝试更小的平均值,收缩二分右边界;
- 无负环:所有环平均值都大于 $mid$,需要放大二分左边界。
3. 二分单调性证明
该问题满足二分单调性:
- 若 $mid_0$ 是可行解(存在环平均$\le mid_0$),则所有 $mid>mid_0$ 一定可行;
- 若 $mid_0$ 不可行(所有环平均$>mid_0$),则所有 $mid<mid_0$ 一定不可行。
因此可以使用浮点二分不断缩小区间,逼近最优实数解。
4. 负环判定选择:DFS-SPFA(深度优先SPFA)
本题 $n$ 上限3000,二分需要循环50次左右,每次都要判负环,对判环效率要求极高:
- BFS队列版SPFA:多次迭代容易超时;
- DFS深搜SPFA:一旦递归回溯时遇到栈内节点,立刻判定负环,剪枝极强,适配本题多次二分场景。
三、完整AC代码
#include <bits/stdc++.h>
using namespace std;
const int N = 3005, M = 10005;
const double eps = 1e-10;
int n, m;
// 链式前向星
int h[N], to[M], ne[M], idx;
double w[M], d[N];
bool vis[N];
// 加边函数
void add(int u, int v, double val) {
to[idx] = v;
w[idx] = val;
ne[idx] = h[u];
h[u] = idx++;
}
// DFS-SPFA 深搜判负环
bool spfa(int u, double x) {
vis[u] = true;
for (int i = h[u]; i; i = ne[i]) {
int v = to[i];
// 边权替换为 w - x
if (d[v] > d[u] + w[i] - x) {
d[v] = d[u] + w[i] - x;
// v在递归栈内 或 子树搜到负环,直接向上返回true
if (vis[v] || spfa(v, x)) {
return true;
}
}
}
vis[u] = false;
return false;
}
// 二分判定:给定mid=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 binary_find() {
double l = -1e7, r = 1e7;
// 精度1e-10,满足输出8位小数无四舍五入误差
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid)) {
// 存在负环,mid可行,尝试更小平均值
r = mid;
} else {
l = mid;
}
}
return r;
}
int main() {
memset(h, 0, sizeof h);
idx = 1;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v;
double val;
scanf("%d%d%lf", &u, &v, &val);
add(u, v, val);
}
double ans = binary_find();
// 固定输出8位小数
printf("%.8lf\n", ans);
return 0;
}
四、代码逐段分层详解
1. 常量、全局数组与链式前向星
const int N = 3005, M = 10005;
const double eps = 1e-10;
int n, m;
int h[N], to[M], ne[M], idx;
double w[M], d[N];
bool vis[N];
void add(int u, int v, double val) {
to[idx] = v;
w[idx] = val;
ne[idx] = h[u];
h[u] = idx++;
}
- 数组上限 $N=3005、M=10005$,严格匹配题目数据范围;
eps=1e-10:二分循环终止精度,高于题目要求的8位小数,消除浮点输出误差;- 链式前向星:存储有向图,节省空间,遍历边效率高;
d[]:SPFA最短路距离数组,存储松弛后的路径权值;vis[]:递归栈标记,标记当前节点是否处于DFS搜索路径中,用于快速判定环。
2. DFS-SPFA 深搜负环核心函数
bool spfa(int u, double x) {
vis[u] = true;
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] = false;
return false;
}
执行流程拆解:
vis[u] = true:将当前节点加入递归栈,标记为正在访问;- 遍历节点 $u$ 所有出边,使用变形边权 $w-x$ 做松弛更新;
- 若成功松弛节点 $v$:
- 若 $vis[v]=true$:$v$ 已经在当前递归栈,说明找到一条从 $v$ 出发回到 $v$ 的负环,直接返回
true; - 递归调用
spfa(v,x),子树一旦找到负环,向上逐层传递true;
- 若 $vis[v]=true$:$v$ 已经在当前递归栈,说明找到一条从 $v$ 出发回到 $v$ 的负环,直接返回
- 回溯阶段
vis[u]=false:退出递归栈,清除标记; - 整棵子树无负环,返回
false。
3. check 二分判定函数
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;
}
memset(d,0x3f):将距离数组初始化为极大值,代表初始不可达;- 清空
vis栈标记,每次二分判定都是全新图; - 多起点循环遍历所有点:防止图存在互不连通的分量,漏掉隐藏负环;
- 任意起点搜索出负环,立刻返回
true代表当前mid可行。
4. binary_find 浮点二分函数
double binary_find() {
double l = -1e7, r = 1e7;
while (r - l > eps) {
double mid = (l + r) / 2;
if (check(mid)) {
r = mid;
} else {
l = mid;
}
}
return r;
}
- 二分区间 $[-1e7,1e7]$:匹配边权取值范围,平均值不会超出该区间;
- 循环条件
r-l>eps:区间缩小到精度阈值后停止; - 分支逻辑:
check(mid)=true:存在环平均值 ≤ mid,当前猜测值偏大,收缩右边界r=mid;check(mid)=false:所有环平均值 > mid,需要放大左边界l=mid;
- 循环结束返回
r,即为最小平均环浮点解。
5. main 主函数输入输出
int main() {
memset(h, 0, sizeof h);
idx = 1;
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int u, v;
double val;
scanf("%d%d%lf", &u, &v, &val);
add(u, v, val);
}
double ans = binary_find();
printf("%.8lf\n", ans);
return 0;
}
- 初始化链式前向星表头,清空数组;
- 读取点数、边数,循环读入每条有向边并存入前向星;
- 调用二分函数计算最小平均环;
printf("%.8lf")格式化输出,强制保留8位小数,匹配题目输出要求。
五、算法时间复杂度分析
- 浮点二分循环次数:区间 $[-1e7,1e7]$,精度 $10^{-10}$,约50次循环;
- 单次check判定:多起点DFS-SPFA,图规模 $n=3000,m=10000$,深搜剪枝后实际遍历远少于全图;
总复杂度:$O(50 \times (n+m))$,数据范围下运行速度极快,无超时风险。
六、题型总结与模板通用性
1. 题型定位
经典01分数规划 + 有向图负环判定综合模板,信息学竞赛高频中档算法题。
2. 固定解题通用流程
- 对分式最值问题做数学变形,构造新边权 $w'=w-mid$;
- 浮点二分枚举目标平均值;
- 根据变形后的图特性,使用DFS-SPFA判定负环,收缩二分区间;
- 达到精度要求后输出答案。
3. 拓展适用场景
所有「求有向图中环的平均权值最大/最小」题目,均可直接套用这套二分+DFS-SPFA模板,仅需调整二分分支收缩方向。
4. 关键细节应试提醒
- 二分精度必须高于输出要求小数位数,避免浮点误差导致取整错误;
- 判负环优先选择DFS版SPFA,二分多次调用场景效率远高于BFS;
- 必须多起点遍历全图,防止孤立连通块漏掉负环;
- 距离数组每次判定都要重新初始化为无穷大,不能复用上次结果。
浙公网安备 33010602011771号