题解:洛谷 P3199 最小圈

【题目来源】

洛谷:P3199 [HNOI2009] 最小圈 - 洛谷

【题目描述】

考虑带权有向图 \(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

【核心思想】

  1. 问题分析:给定一个有向带权图,需要找出所有有向圈中边权平均值的最小值。直接枚举所有圈不可行,而该问题可通过二分答案转化为判定性问题:是否存在一个圈的平均值小于给定的阈值 x。利用“减去 x 后判定负环”的经典技巧,可将原优化问题转化为多次判负环问题。

  2. 算法选择

    • 二分答案:最小值 μ 的取值范围在边权范围内(通常设 \([-10^7, 10^7]\)),且具有单调性——若存在平均值 ≤ x 的圈,则任意更大的 x 也满足,因此可以二分搜索最小可行 x。
    • 判负环(SPFA):对于每次猜测的 mid,将每条边的权值减去 mid,然后使用 SPFA 检测图中是否存在负环。若存在负环,则说明存在圈的平均值小于 mid,答案应在左半区间;否则在右半区间。
    • DFS 版 SPFA:利用深度优先搜索检测负环,可提前终止,效率较高。
  3. 关键步骤

    • 读入与建图:读取 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 位小数)。
  4. 时间/空间复杂度

    • 时间复杂度:\(O(\log(\text{范围}/\text{精度}) \cdot (n \cdot m))\),二分约 60 次,每次 SPFA 最坏 \(O(nm)\),实际较快。
    • 空间复杂度:\(O(n + m)\),存储图和辅助数组。
  5. 二分答案 + 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
posted @ 2026-09-02 21:40  团爸讲算法  阅读(6)  评论(0)    收藏  举报