图论之判断负环

本博客贴出两种常见的 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;
}
posted @ 2022-06-20 19:58  lucky_light  阅读(640)  评论(0编辑  收藏  举报