题解:洛谷 P3385 负环

【题目来源】

洛谷:P3385 【模板】负环 - 洛谷

【题目描述】

给定一个 \(n\) 个点的有向图,请求出图中是否存在从顶点 \(1\) 出发能到达的负环。

负环的定义是:一条边权之和为负数的回路。

【输入】

输入的第一行是一个整数 \(T\),表示测试数据的组数。对于每组数据的格式如下:

第一行有两个整数,分别表示图的点数 \(n\) 和接下来给出边信息的条数 \(m\)

接下来 \(m\) 行,每行三个整数 \(u,v,w\)

  • \(w≥0\),则表示存在一条从 \(u\)\(v\) 边权为 \(w\) 的边,还存在一条从 \(v\)\(u\) 边权为 \(w\) 的边。
  • \(w<0\),则只表示存在一条从 \(u\)\(v\) 边权为 \(w\) 的边。

【输出】

对于每组数据,输出一行一个字符串,若所求负环存在,则输出 YES,否则输出 NO

【输入样例】

2
3 4
1 2 2
1 3 4
2 3 1
3 1 -3
3 3
1 2 3
2 3 4
3 1 -8

【输出样例】

NO
YES

【核心思想】

  1. 问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向图,边权可能为负。需要判断是否存在从顶点 1 出发能到达的负环(即一条边权之和为负数的回路)。由于边权可负,Dijkstra 算法失效,需使用能处理负权的 Bellman-Ford 或 SPFA 算法。本题是经典的负环检测问题,关键在于利用松弛操作次数来判断是否有负环。

  2. 算法选择

    • SPFA(队列优化的 Bellman-Ford):利用队列维护待松弛的节点,记录每个节点的入队次数(或松弛次数)。若某个节点入队次数超过 \(n\),则说明存在负环。SPFA 在大多数情况下效率较高,适合处理稀疏图。
    • Bellman-Ford(备选):进行 \(n\) 轮松弛,若第 \(n\) 轮仍能松弛,则存在负环。虽然复杂度 \(O(nm)\) 稍高,但实现简单,本题中也可通过。
    • 建图特殊规则:若边权 \(w \ge 0\),则添加双向边(即 \(u \to v\)\(v \to u\) 两条边);若 \(w < 0\),则只添加单向边 \(u \to v\)
  3. 关键步骤(以 SPFA 判负环为例):

    • 建图:读入 \(n, m\),对于每条边 \((u, v, w)\),若 \(w \ge 0\),添加两条边 add(u, v, w)add(v, u, w);否则只添加 add(u, v, w)
    • 初始化:距离数组 dist[1..n] = 0(或 INF,但置 0 也可),计数数组 cnt[1..n] = 0,标记数组 st[1..n] = true,将所有节点入队(因为要检测从任意点出发的负环,但题目要求从 1 可达,入队全部点可覆盖所有连通分量)。
    • SPFA 主循环
      • 取出队首节点 u,标记 st[u] = false
      • 遍历 u 的所有出边 (v, w)
        • dist[v] > dist[u] + w
          • 更新 dist[v] = dist[u] + w
          • cnt[v] = cnt[u] + 1
          • cnt[v] >= n,说明存在负环,返回 true
          • v 不在队列中,将其入队并标记。
    • 判定输出:若 SPFA 返回 true,且从 1 出发可达负环(题目要求从 1 可达),需额外判断 h[1] != -1(即 1 有出边,确保负环在 1 的可达范围内)。若满足条件输出 YES,否则输出 NO
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n \cdot m)\)(SPFA 最坏情况),但实际运行较快。
    • 空间复杂度:\(O(n + m)\),邻接表存储图。
  5. SPFA 判负环的核心思想

    • 松弛次数与最短路径长度:若无负环,从源点到任意点的最短路径最多经过 \(n-1\) 条边,因此每个节点最多被松弛 \(n-1\) 次。若某节点被松弛了 \(n\) 次,说明存在一条边数至少为 \(n\) 的更短路径,即存在负环。
    • 全部节点入队:为了检测从 1 可达的负环,将所有节点初始入队并设 dist=0,可以同时检测所有连通分量中的负环;但最终需结合从 1 出发的可达性进行筛选。
    • 双向边处理:非负边权意味着可以双向通行,这在构造图中需正确反映。
    • 适用场景:适用于检测带负权有向图中的负环,是图论中的基础模板题。

【算法标签】

普及 #SPFA

【代码详解】

//Ford 判负环 740ms
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cstring>  // 需要包含memset
using namespace std;

const int inf = 0x3f3f3f3f;  // 定义无穷大
const int N = 2010;          // 最大顶点数
const int M = 6010;          // 最大边数
int n, m;                    // 顶点数,边数
int to[M], ne[M], w[M], h[N], tot;  // 链式前向星
int d[N];                  // 最短距离数组

/**
 * 添加有向边
 * @param a 起点
 * @param b 终点
 * @param c 权重
 */
void add(int a, int b, int c)
{
    to[++tot] = b;   // 边指向的顶点
    w[tot] = c;      // 边的权重
    ne[tot] = h[a];  // 指向原链表头
    h[a] = tot;      // 更新头指针
}

/**
 * Bellman-Ford算法实现,检测负权环
 * 原理:如果没有负权环,最多n-1轮松弛可得到最短路径
 * 如果第n轮还能松弛,说明存在负权环
 * @return 存在负权环返回true,否则返回false
 */
bool ford()
{
    memset(d, inf, sizeof d);  // 初始化距离为无穷大
    d[1] = 0;  // 假设起点为1,距离为0

    bool flag;  // 标记本轮是否进行了松弛操作

    // 最多进行n轮松弛
    for (int i = 1; i <= n; i++)  // 跑n轮
    {
        flag = false;  // 初始化为未松弛

        // 遍历所有顶点
        for (int u = 1; u <= n; u++)  // n个点
        {
            if (d[u] == inf)  // 如果当前顶点不可达,跳过
            {
                continue;
            }

            // 遍历u的所有出边
            for (int j = h[u]; j; j = ne[j])
            {
                int v = to[j];  // 邻接顶点

                // 松弛操作
                if (d[v] > d[u] + w[j])
                {
                    d[v] = d[u] + w[j];
                    flag = true;  // 标记有松弛
                }
            }
        }

        // 如果本轮没有松弛,提前结束
        if (!flag)
        {
            break;
        }
    }

    // 如果第n轮还能松弛,说明存在负权环
    return flag;  // 第n轮=true,有负环
}

int main()
{
    int T;  // 测试用例数量
    scanf("%d", &T);

    while (T--)
    {
        // 初始化邻接表
        tot = 0;
        memset(h, 0, sizeof(h));

        // 输入顶点数和边数
        scanf("%d%d", &n, &m);

        // 读入边
        for (int i = 1; i <= m; i++)
        {
            int u, v, w;
            scanf("%d%d%d", &u, &v, &w);

            add(u, v, w);  // 添加有向边

            // 如果是非负边,添加反向边(构成无向边)
            if (w >= 0)
            {
                add(v, u, w);
            }
        }

        // 检测负权环并输出结果
        puts(ford() ? "YES" : "NO");
    }

    return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;

const int N = 2005;   // 最大顶点数
const int M = 6005;   // 最大边数

int h[N], e[M], w[M], ne[M], idx;  // 链式前向星存储图
int dist[N];    // 最短距离数组
int cnt[N];     // 记录每个顶点被松弛的次数
bool st[N];     // 标记顶点是否在队列中
int T, n, m;    // T: 测试用例数, n: 顶点数, m: 边数

/**
 * 添加有向边
 * @param a 起点
 * @param b 终点
 * @param c 权重
 */
void add(int a, int b, int c)
{
    e[idx] = b;        // 边指向的顶点
    w[idx] = c;        // 边的权重
    ne[idx] = h[a];    // 指向原链表头
    h[a] = idx++;      // 更新头指针
}

/**
 * SPFA算法检测负权环
 * 从所有顶点开始检测,确保能找到任意连通分量中的负权环
 * @return 存在负权环返回true,否则返回false
 */
int spfa()
{
    queue<int> q;

    // 初始化
    memset(dist, 0, sizeof(dist));  // 距离初始化为0
    memset(cnt, 0, sizeof(cnt));    // 松弛次数初始化为0

    // 将所有顶点入队
    for (int i = 1; i <= n; i++)
    {
        st[i] = true;  // 标记在队列中
        q.push(i);     // 顶点入队
    }

    // SPFA主循环
    while (q.size())
    {
        int t = q.front();  // 取出队首顶点
        q.pop();
        st[t] = false;      // 标记不在队列中

        // 遍历t的所有邻接边
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];  // 邻接顶点

            // 松弛操作
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];  // 更新最短距离
                cnt[j] = cnt[t] + 1;       // 松弛次数+1

                // 如果顶点j被松弛了n次,说明存在负权环
                if (cnt[j] >= n)
                {
                    return true;  // 存在负权环
                }

                // 如果j不在队列中,入队
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;  // 不存在负权环
}

int main()
{
    // 输入测试用例数量
    cin >> T;

    while (T--)
    {
        // 输入顶点数和边数
        cin >> n >> m;

        // 初始化邻接表
        memset(h, -1, sizeof(h));
        idx = 0;  // 重置边索引

        // 读入边
        for (int i = 1; i <= m; i++)
        {
            int a, b, c;
            cin >> a >> b >> c;

            add(a, b, c);  // 添加有向边

            // 如果是非负边,添加反向边(构成无向边)
            if (c >= 0)
            {
                add(b, a, c);
            }
        }

        /**
         * 判断是否存在从顶点1出发可达的负权环
         * 条件:
         * 1. 存在负权环 (!spfa() 返回false时表示有负环)
         * 2. 顶点1必须有出边 (h[1] != -1)
         * 注意:spfa()返回true表示有负环,false表示无负环
         * 但题目要求"从1出发不存在负环"时输出"NO"
         * 这里条件为:!spfa() || h[1] == -1
         * 即:无负环 或 顶点1无出边
         */
        if (!spfa() || h[1] == -1)  // 注意:spfa()返回true表示有负环
        {
            cout << "NO" << endl;  // 题目要求是从1出发不存在负环
        }
        else
        {
            cout << "YES" << endl;
        }
    }

    return 0;
}

【运行结果】

2
3 4
1 2 2
1 3 4
2 3 1
3 1 -3
NO
3 3
1 2 3
2 3 4
3 1 -8
YES
posted @ 2026-09-01 16:52  团爸讲算法  阅读(18)  评论(0)    收藏  举报