图论之判断负环
本博客贴出两种常见的 Spfa 判断负环的方法。
题目链接
spfa 超级源点判断
参考 Y 总做法
/*
* 判断图中负环
*
* 判断负环是一个比较经典的问题,图中是否存在负环甚至是某些算法应用的先决条件,首先我们探讨一下什么是什么是环。
*
* 环
* 环使之从一个点出发,经过其他点能,最后能够折返回到该点。环是相对于有向图而言的。因为无向图的话,倘若存在边,就存在环。
* 有向图判断是否存在环,可以通过 TopSort (拓扑排序),通过入度 in degree 的统计,复杂度是 O(M)
* 有向图是否存在环,也可以使用 DFS 来判断,从一个点出发,然后查看是否会重复经过自己。复杂度是 O(NM)
* 这里需要主要其中一个优化思想是错的,有些人认为在搜寻过程中,重复经过任意点就是环,
* 如 1->2, 1->3, 2->3 图中, 3这个点重复经过了,但不存在环。因此复杂度需要 O(NM)。
*
* 综上,环的判断方法主要是 DFS 和 TopSort,复杂度分别是 O(NM) 和 O(M)
* 负环
* 负环,是在环的基础之上,进一步要求环上边的权重之和为负数。
* 从定义来看,负环不仅仅和边的存在还和边的权重相关。判断是否存在的算法,我个人感觉有以下几种(我并没有搜索)
* 1. DFS,这次DFS 复杂度会比 环DFS 复杂度高,因为环只需要判断是否可以到达该点,此时我们需要加上权重的衡量,距离 dist 要小。
* 2. 根据最短路的边长来看
* * Bellman-Ford在没有负环的前提下,最多运行 n - 1次循环,一定循环次数大于等于 n 即说明存在负环,使得最短路路径 >= n 复杂度 O(NM)
* * 不论是什么算法,一定最短路的路径大于等于 n 的话,就说明存在负环
* * spfa 算法,以任意原点为起点,倘若存在其他点路径 >= n 就说明存在负环,否则其他未经过的点为起点,重复进行。
* * 复杂度 O(NM),并且往往达不到 O(NM),这是因为以 u 点为原点,不存在负环的话,那么说明 所有 U 可达的点,都不存在负环,
* * 一个集合一个集合的遍历,复杂度自然是很低。
* * - spfa 另一种 spfa 可以直接,相当于直接开启一个超级远点 O 点,距离其他点为 0,查看其他点是否存在负环的情况。
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010, INF = 0x3f3f3f3f;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool super_src_judge_negative_circle() {
// initialize
memset(st, false, sizeof st);
memset(dist, 0, sizeof dist); // 直接 dist = 0,或者是任意常数即可,主要是怕 INF 越界
memset(cnt, 0x3f, sizeof cnt);
queue<int> que;
for (int i = 1; i <= n; i ++ ) {
st[i] = true;
cnt[i] = 0;
que.push(i);
}
// 倘若 visited 的点,就不必再次经过了,因为他身上绝不会存在负环。
// 一旦经过了负环上的点,那么一点按照负环环绕,最短路径必然会大于 n
int u, v;
while (que.empty() == false) {
u = que.front(); que.pop();
st[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
v = e[i];
if (dist[v] > dist[u] + w[i]) {
dist[v] = dist[u] + w[i];
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { // 出现负环
return true;
}
if (st[v] == false) {
st[v] = true;
que.push(v);
}
}
}
}
return false;
}
int main()
{
// initialize
memset(h, -1, sizeof h);
memset(ne, -1, sizeof ne);
idx = 0;
// input
scanf("%d%d", &n, &m);
int a, b, c;
while (m -- ) {
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if (super_src_judge_negative_circle()) {
printf("Yes\n");
} else {
printf("No\n");
}
return 0;
}
spfa 记忆数组实现
自己基于记忆数组的想法
/*
* 判断图中负环
*
* 判断负环是一个比较经典的问题,图中是否存在负环甚至是某些算法应用的先决条件,首先我们探讨一下什么是什么是环。
*
* 环
* 环使之从一个点出发,经过其他点能,最后能够折返回到该点。环是相对于有向图而言的。因为无向图的话,倘若存在边,就存在环。
* 有向图判断是否存在环,可以通过 TopSort (拓扑排序),通过入度 in degree 的统计,复杂度是 O(M)
* 有向图是否存在环,也可以使用 DFS 来判断,从一个点出发,然后查看是否会重复经过自己。复杂度是 O(NM)
* 这里需要主要其中一个优化思想是错的,有些人认为在搜寻过程中,重复经过任意点就是环,
* 如 1->2, 1->3, 2->3 图中, 3这个点重复经过了,但不存在环。因此复杂度需要 O(NM)。
*
* 综上,环的判断方法主要是 DFS 和 TopSort,复杂度分别是 O(NM) 和 O(M)
* 负环
* 负环,是在环的基础之上,进一步要求环上边的权重之和为负数。
* 从定义来看,负环不仅仅和边的存在还和边的权重相关。判断是否存在的算法,我个人感觉有以下几种(我并没有搜索)
* 1. DFS,这次DFS 复杂度会比 环DFS 复杂度高,因为环只需要判断是否可以到达该点,此时我们需要加上权重的衡量,距离 dist 要小。
* 2. 根据最短路的边长来看
* * Bellman-Ford在没有负环的前提下,最多运行 n - 1次循环,一定循环次数大于等于 n 即说明存在负环,使得最短路路径 >= n 复杂度 O(NM)
* * 不论是什么算法,一定最短路的路径大于等于 n 的话,就说明存在负环
* * spfa 算法,以任意原点为起点,倘若存在其他点路径 >= n 就说明存在负环,否则其他未经过的点为起点,重复进行。
* * 复杂度 O(NM),并且往往达不到 O(NM),这是因为以 u 点为原点,不存在负环的话,那么说明 所有 U 可达的点,都不存在负环,
* * 一个集合一个集合的遍历,复杂度自然是很低。
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <string>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010, INF = 0x3f3f3f3f;
int n, m;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], cnt[N];
bool st[N], visited[N];
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
bool judge_negative_circle(int src) {
// initialize
memset(st, false, sizeof st);
memset(dist, 0x3f, sizeof dist);
memset(cnt, 0x3f, sizeof cnt);
st[src] = true;
dist[src] = 0;
cnt[src] = 0;
queue<int> que;
que.push(src);
// 倘若 visited 的点,就不必再次经过了,因为他身上绝不会存在负环。
// 一旦经过了负环上的点,那么一点按照负环环绕,最短路径必然会大于 n
int u, v;
while (que.empty() == false) {
u = que.front(); que.pop();
st[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
v = e[i];
if (visited[v] == true) { // visited[u] = true, 那么就没必要遍历了
continue;
} else {
if (dist[v] > dist[u] + w[i]) {
dist[v] = dist[u] + w[i];
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { // 出现负环
return true;
}
if (st[v] == false) {
st[v] = true;
que.push(v);
}
}
}
}
}
// 更新
for (int i = 1; i <= n; i ++ ) {
if (dist[i] <= INF / 2) {
visited[i] = true;
}
}
return false;
}
int main()
{
// initialize
memset(h, -1, sizeof h);
memset(ne, -1, sizeof ne);
idx = 0;
// input
scanf("%d%d", &n, &m);
int a, b, c;
while (m -- ) {
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
bool exist_negative_circle = false;
memset(visited, false, sizeof visited);
for (int i = 1; i <= n; i ++ ) {
if (visited[i] == false) {
// visited[i] = true; 不应该放在这里,应该在 judge_negative_circle 函数后面补充
if (judge_negative_circle(i)) {
exist_negative_circle = true;
break;
}
}
}
if (exist_negative_circle) {
printf("Yes\n");
} else {
printf("No\n");
}
return 0;
}