【洛谷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. 输入输出规则

输入:

  1. 第一行两个整数 $n,m$,代表图中点数、有向边条数;
  2. 后续 $m$ 行,每行三个数 $i,j,w_{i,j}$,代表一条有向边 $i\to j$,边权为 $w$;
  3. 数据保证图连通、至少存在一个环,存在点可抵达其余所有点。

输出:
输出最小环平均值,严格保留小数点后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$ 的环,分为两种情况:

  1. 存在负环(总权 $<0$):完全满足条件,$mid$ 可行;
  2. 存在零环(总权 $=0$):刚好取到等于 $mid$ 的环,$mid$ 同样可行。

综上,判定逻辑简化为:新权图中是否存在负环

  • 存在负环:当前 $mid$ 偏大,我们可以尝试更小的平均值,收缩二分右边界;
  • 无负环:所有环平均值都大于 $mid$,需要放大二分左边界。

3. 二分单调性证明

该问题满足二分单调性:

  1. 若 $mid_0$ 是可行解(存在环平均$\le mid_0$),则所有 $mid>mid_0$ 一定可行;
  2. 若 $mid_0$ 不可行(所有环平均$>mid_0$),则所有 $mid<mid_0$ 一定不可行。
    因此可以使用浮点二分不断缩小区间,逼近最优实数解。

4. 负环判定选择:DFS-SPFA(深度优先SPFA)

本题 $n$ 上限3000,二分需要循环50次左右,每次都要判负环,对判环效率要求极高:

  1. BFS队列版SPFA:多次迭代容易超时;
  2. 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++;
}
  1. 数组上限 $N=3005、M=10005$,严格匹配题目数据范围;
  2. eps=1e-10:二分循环终止精度,高于题目要求的8位小数,消除浮点输出误差;
  3. 链式前向星:存储有向图,节省空间,遍历边效率高;
  4. d[]:SPFA最短路距离数组,存储松弛后的路径权值;
  5. 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;
}

执行流程拆解:

  1. vis[u] = true:将当前节点加入递归栈,标记为正在访问;
  2. 遍历节点 $u$ 所有出边,使用变形边权 $w-x$ 做松弛更新;
  3. 若成功松弛节点 $v$:
    • 若 $vis[v]=true$:$v$ 已经在当前递归栈,说明找到一条从 $v$ 出发回到 $v$ 的负环,直接返回true
    • 递归调用spfa(v,x),子树一旦找到负环,向上逐层传递true
  4. 回溯阶段 vis[u]=false:退出递归栈,清除标记;
  5. 整棵子树无负环,返回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;
}
  1. memset(d,0x3f):将距离数组初始化为极大值,代表初始不可达;
  2. 清空vis栈标记,每次二分判定都是全新图;
  3. 多起点循环遍历所有点:防止图存在互不连通的分量,漏掉隐藏负环;
  4. 任意起点搜索出负环,立刻返回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;
}
  1. 二分区间 $[-1e7,1e7]$:匹配边权取值范围,平均值不会超出该区间;
  2. 循环条件 r-l>eps:区间缩小到精度阈值后停止;
  3. 分支逻辑:
    • check(mid)=true:存在环平均值 ≤ mid,当前猜测值偏大,收缩右边界r=mid
    • check(mid)=false:所有环平均值 > mid,需要放大左边界l=mid
  4. 循环结束返回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;
}
  1. 初始化链式前向星表头,清空数组;
  2. 读取点数、边数,循环读入每条有向边并存入前向星;
  3. 调用二分函数计算最小平均环;
  4. printf("%.8lf") 格式化输出,强制保留8位小数,匹配题目输出要求。

五、算法时间复杂度分析

  1. 浮点二分循环次数:区间 $[-1e7,1e7]$,精度 $10^{-10}$,约50次循环;
  2. 单次check判定:多起点DFS-SPFA,图规模 $n=3000,m=10000$,深搜剪枝后实际遍历远少于全图;
    总复杂度:$O(50 \times (n+m))$,数据范围下运行速度极快,无超时风险。

六、题型总结与模板通用性

1. 题型定位

经典01分数规划 + 有向图负环判定综合模板,信息学竞赛高频中档算法题。

2. 固定解题通用流程

  1. 对分式最值问题做数学变形,构造新边权 $w'=w-mid$;
  2. 浮点二分枚举目标平均值;
  3. 根据变形后的图特性,使用DFS-SPFA判定负环,收缩二分区间;
  4. 达到精度要求后输出答案。

3. 拓展适用场景

所有「求有向图中环的平均权值最大/最小」题目,均可直接套用这套二分+DFS-SPFA模板,仅需调整二分分支收缩方向。

4. 关键细节应试提醒

  1. 二分精度必须高于输出要求小数位数,避免浮点误差导致取整错误;
  2. 判负环优先选择DFS版SPFA,二分多次调用场景效率远高于BFS;
  3. 必须多起点遍历全图,防止孤立连通块漏掉负环;
  4. 距离数组每次判定都要重新初始化为无穷大,不能复用上次结果。

posted on 2026-07-09 22:12  5iCode  阅读(5)  评论(0)    收藏  举报

导航