P3385 【模板】负环 题解

· · 个人记录

题目传送门

题意:

负环的定义:若图中存在一个环,环上各边的边权和为负数,则称该环为负环。

分析:

含有负环的图显然是没有最短路的,因为每走一圈这个负环,总权值就会更小,最后一直循环,导致陷在环里出不来。

注意:会看题目,数据中不只是m条边,最多可以有2m条边,边数组一定要开大。

因此,我们可以用 Bellman-FordSPFA 来判断负环。

(从某种意义上来将 SPFA 基本上可以取代 Bellman-Ford。所以本题解就不展开来讲Bellman-Ford了。)

_补:

------------ ### $Bellman-Ford$ 判断负环: $Bellman-Ford$ 算法通过不断迭代计算最短路,每轮迭代至少有一个结点得到了最短路。所以,若图中没有负环,则最多经过 $n-1$ 轮迭代后算法结束。**若第 $n$ 轮迭代仍有结点的最短路能被更新,则图中有负环。** 复杂度为 $O(nm)$。 ### $SPFA$ 判断负环: $SPFA$ 算法通过不断更新从源点到达各点的距离,使其加入待处理点的队列并调用进行松弛操作。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。对于判断负环有**两种**表达方式。理论上时间复杂度都为$O(KE)$,K为常数,平均值为2,**最坏为$O(NM)$** #### 方法一: 若图中没有负环,则最坏情况下每轮找到一个更优一点的节点(包括自己),所以每个点最多经过 $n$ 轮迭代后算法结束。**若某个点经过超过 $n$ 轮迭代仍有结点的最短路能被更新,则图中有负环**。 ## 代码: ```cpp #include<bits/stdc++.h> #define ll long long using namespace std; const ll N = 6e3 + 20, inf = 1e16 + 20;; ll n, m, u, v, w, T, s = 1; ll cnt = 0, to[N], val[N], nxt[N], h[N]; ll vis[N], dis[N]; bool flag[N]; struct data{ ll u, dis; bool operator < (const data a) const { return a.dis < dis; } }; queue<data> q; void add_edge(ll u, ll v, ll w) { to[++cnt] = v; val[cnt] = w; nxt[cnt] = h[u]; h[u] = cnt; } void init(){ memset(to, 0, sizeof(to)); memset(dis, 0, sizeof(dis)); memset(val, 0, sizeof(val)); memset(h, 0, sizeof(h)); cnt = 0; for(int i = 1; i <= n; i++ )dis[i] = inf; dis[s] = 0; memset(vis, 0, sizeof(vis)); memset(flag, 0, sizeof(flag)); while(!q.empty()) q.pop(); } bool Spfa(ll s) { q.push((data){s, 0}); flag[s] = 1; while(!q.empty()) { ll u = q.front().u; q.pop(); flag[u] = 0; vis[u] ++ ; if(vis[u] > n) return 1;//判断负环 for(int i = h[u]; i; i = nxt[i]) { ll v = to[i]; if(dis[v] > dis[u] + val[i]) { dis[v] = dis[u] + val[i]; if(flag[v] == 0) { q.push((data){v, dis[v]}); flag[v] = 1; } } } } return 0; } int main (){ scanf("%lld", &T); while(T--) { scanf("%lld%lld", &n, &m); init();//------->清零 for(int i = 1; i <= m; i++) { scanf("%lld%lld%lld", &u, &v, &w); if(w >= 0) { add_edge(u, v, w); add_edge(v, u, w); } else add_edge(u, v, w); } if(Spfa(s) == 1) printf("YES\n"); else printf("NO\n"); } return 0; } ``` ### [测评](https://www.luogu.com.cn/record/99624743)用时$ 1.31 s

缺点:本方法效率不高,如果数据使得n个点在最后返回前都被访问n次,那么本程序的效率将会变得很低。那么如何进行简单的优化呢?且看方法二。

方法二:

众所周知,若图中没有负环,则从源点到达除源点外的另一个节点的路径最多包含n(就像链一样)。若某个路径经过超过 n 个节点,则图中有负环

*该方法避免了方法一的最坏情况,大大提高了判断负环的效率,虽理论上时间复杂度一样,但实际上方法二是优于方法一的。

代码:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N = 6e3 + 20, inf = 1e16 + 20;
ll T, n, m, u, v, w, s = 1;
ll cnt = 0, h[N], nxt[N], val[N], to[N];
ll vis[N], dis[N];
bool flag[N];
queue<ll> q;
void add_edge(ll u, ll v, ll w) {
    to[++cnt] = v;
    val[cnt]  = w;
    nxt[cnt]  = h[u];
    h[u]      = cnt;
}
void init(){
    memset(to, 0, sizeof(to));
    memset(val, 0, sizeof(val));
    memset(h, 0, sizeof(h));
    memset(nxt, 0, sizeof(nxt));
    memset(flag, 0, sizeof(flag));
    memset(vis, 0, sizeof(vis));
    cnt = 0;
    for(int i = 1; i <= n; i++) dis[i] = inf;
    dis[s] = 0;
    while(!q.empty()) q.pop();

}
bool SPFA(ll s) {
    q.push(s);
    vis[s] = flag[s] = 1;
    while(!q.empty()){
        ll u = q.front();
        q.pop();
        flag[u] = 0;

        for(int i = h[u]; i; i = nxt[i]){
            ll v = to[i];
            if(dis[v] > dis[u] + val[i]) {
                dis[v] = dis[u] + val[i];
                vis[v] = vis[u] + 1;
                if(vis[u] > n) return 1;//判断负环
                if(flag[v] == 0) {
                    q.push(v);
                    flag[v] = 1;
                }
            }
        }
    }
    return 0;
}
int main (){
    scanf("%lld", &T);
    while(T--) {
        scanf("%lld%lld", &n, &m);
        init();
        for(int i = 1; i <= m; i++) {
            scanf("%lld%lld%lld", &u, &v, &w);
            if(w >= 0) {
                add_edge(u, v, w);
                add_edge(v, u, w);
            }
            else add_edge(u, v, w);
        }
        if(SPFA(s) == 1) printf("YES\n");
        else             printf("NO\n");

    }
    return 0;
}

测评用时 571ms