题解:洛谷 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
【核心思想】
-
问题分析:给定一个 \(n\) 个点、\(m\) 条边的有向图,边权可能为负。需要判断是否存在从顶点 1 出发能到达的负环(即一条边权之和为负数的回路)。由于边权可负,Dijkstra 算法失效,需使用能处理负权的 Bellman-Ford 或 SPFA 算法。本题是经典的负环检测问题,关键在于利用松弛操作次数来判断是否有负环。
-
算法选择:
- 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\)。
-
关键步骤(以 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。
- 建图:读入 \(n, m\),对于每条边 \((u, v, w)\),若 \(w \ge 0\),添加两条边
-
时间/空间复杂度:
- 时间复杂度:\(O(n \cdot m)\)(SPFA 最坏情况),但实际运行较快。
- 空间复杂度:\(O(n + m)\),邻接表存储图。
-
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
浙公网安备 33010602011771号