图论之判断负环
本博客贴出两种常见的 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; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)