图论-SPFA算法学习笔记

SPFA 算法学习笔记

核心思想

SPFA 是 Bellman-Ford 算法的队列优化版本。

就记住 SPFA 搭配 queue<int>inq[] 标记就对了:

  • 当你把一个点加入队列时,inq[u] = 1
  • 弹出来时,inq[u] = 0
  • 判负环时,cnt[v] = cnt[u] + 1

模板代码

bool spfa()  // 判断负环
{
    memset(d, 0x3f, sizeof(d));   // 初始化三兄弟
    memset(inq, 0, sizeof(inq));
    memset(cnt, 0, sizeof(cnt));
    queue<int>q;
  
    q.push(1), inq[1] = 1;        // 进队列了
    d[1] = 0;                     // 起点为0
  
    while(q.size())
    {
        int u = q.front(); q.pop();
        inq[u] = 0;
    
        for(auto x : e[u])        // 遍历u就行
        {
            int v = x.v, w = x.w;
            if(d[v] > d[u] + w)   // 与 Dijkstra 同理(这里是 >,不要 '=')
            {
                d[v] = d[u] + w;
                cnt[v] = cnt[u] + 1;   // 从起点到v的路径上有几条边
                if(cnt[v] >= n) return 1;  // 判断边数
                // n条边如果在正常图里就是把所有点都跑了一遍,还倒贴一个点
                // 你说是不是环
                if(inq[v] == 0) q.push(v), inq[v] = 1;
                // 加点,与 Dijkstra 同理
            }
        }
    }
    return 0;
}

基础题

🟡 P3385 【模板】负环

链接:P3385 负环

就是标准的判断负环,SPFA 模板直接上。


变式:差分约束

差分约束的核心思想:把不等式转化为图论最短路问题。

🟡 P5960 【模板】差分约束

链接:P5960 差分约束

题目给出:

\[a - b \le C \]

经过转化,你可以得到:

\[a \le b + C \]

\(a, b\) 当成 \(u, v\) 两点,和最短路基本式高度相似:\(d[v] \le d[u] + w\)

也就是我们可以建一条 \(b \to a\),边权为 \(C\) 的边。

但还没完,这样子图不保证连通,从数学角度来说是无特解。

所以我们建一个超级源点 \(O \to i\)

for(int i = 1; i <= n; i++) e[0].push_back({i, 0});  // 超级源点

在数学意义上是规定 \(i \le O + 0\),即 \(i \le 0\)

这样这道题目就跑得通了。

注意:加上一个超级源点后,总点数为 \(n + 1\),所以判负环时要用 cnt[v] >= n + 1


🟢 P1993 小 K 的农场

链接:P1993 小K的农场

也是数学上的变换:

  1. \(a - b \ge C\),即 \(b \le a - C\),建边 \(a \to b\),边权为 \(-C\)
  2. \(a - b \le C\),即 \(a \le b + C\),建边 \(b \to a\),边权为 \(C\)
  3. \(a = b\),则 \(a - b \ge 0\)\(a - b \le 0\),两条边同时建即可

一句话总结:SPFA 就是队列优化的 Bellman-Ford,核心是 cnt[v] = cnt[u] + 1 判负环。差分约束就是把不等式 \(x_u - x_v \le w\) 转化成边 \(v \to u\),边权为 \(w\)


附:完整代码

P3385 负环

// 请适应这种难受的生活吧
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int inf=INT_MAX;
typedef long long ll;
typedef pair<int,int> pii;

struct edge{int v,w;};
vector<edge>e[N];

int d[N];
bool inq[N];
int cnt[N];

int T,n,m;

bool spfa(int s)
{
    queue<int>q;
    memset(d,0x3f,sizeof(d));
    memset(cnt,0,sizeof(cnt));
    memset(inq,0,sizeof(inq));

    d[s]=0;
    q.push(s);
    inq[s]=1;

    while(q.size())  // [新增] 这里加个";"会查不出来,特征是全循环
    {
        int u=q.front();q.pop();
        inq[u]=0;

        for(auto x:e[u])
        {
            int v=x.v,w=x.w;
            if(d[v]>d[u]+w)  // [新增] 核心:松弛操作
            {
                d[v]=d[u]+w;
                cnt[v]=cnt[u]+1;  // [新增] 记录最短路经过的边数
                if(cnt[v]>=n) return 1;  // [新增] 存在负环
                if(inq[v]==0) q.push(v),inq[v]=1;
            }
        }
    }
    return 0;
}

int main()
{
    #ifndef ONLINE_JUDGE
        freopen("c.in","r",stdin);
        freopen("c.out","w",stdout);
    #endif
    // BEGIN!
    cin>>T;
    while(T--)
    {
        cin>>n>>m;
        for(int i=1,u,v,w;i<=m;i++)
        {
            cin>>u>>v>>w;
            if(w>=0)  // [新增] 非负边权建双向边
            {
                e[u].push_back({v,w});
                e[v].push_back({u,w});
            }
            else e[u].push_back({v,w});  // [新增] 负边权只建单向边
        }
        if(spfa(1)) cout<<"YES"<<endl;
        else cout<<"NO"<<endl;
        memset(e,0,sizeof(e));  // [新增] 清空图,准备下一组数据
    }

    return 0;
}

P5960 差分约束

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int inf=INT_MAX;
typedef long long ll;
typedef pair<int,int> pii;

int n,m;
int d[N];

struct edge{int v,w;};
vector<edge>e[N];

bool inq[N];
int cnt[N];

bool spfa(int s)
{
    memset(d,0x3f,sizeof(d));
    memset(inq,0,sizeof(inq));
    memset(cnt,0,sizeof(cnt));
  
    queue<int>q;
    d[s]=0; q.push(s); inq[s]=1;  // [新增] 入队

    while(q.size())
    {
        int u=q.front(); q.pop();
        inq[u]=0;  // [新增] 弹出点

        for(auto x:e[u])
        {
            int v=x.v,w=x.w;
            if(d[v] >= d[u] + w)  // [新增] 在 >= 的情况下松弛
            {
                d[v]=d[u]+w;
                cnt[v]=cnt[u]+1;  // [新增] 累计边数
                if(cnt[v] >= n+1) return 1;  // [新增] 判负环,n+1是因为加了超级源点
                if(inq[v]==0) q.push(v), inq[v]=1;
            }
        }
    }
    return 0;
}

int main()
{
    #ifndef ONLINE_JUDGE
        freopen("c.in","r",stdin);
        freopen("c.out","w",stdout);
    #endif
    // BEGIN!
    cin>>n>>m;
    for(int i=1,u,v,w;i<=m;i++)
    {
        cin>>u>>v>>w;
        e[v].push_back({u,w});  // [新增] 反向建图:v -> u,边权 w
        // [新增] 原式:x_u - x_v <= w => x_u <= x_v + w
    }
    for(int i=1;i<=n;i++) e[0].push_back({i,0});  // [新增] 超级源点

    if(spfa(0)) cout<<"NO"<<endl;  // [新增] 有负环则无解
    else for(int i=1;i<=n;i++) cout<<d[i]<<' ';  // [新增] 输出一组解

    return 0;
}

注意:差分约束要建超级源点,因为如果 \((x_1, x_2, x_3)\) 是一组解,则 \((x_1 + C, x_2 + C, x_3 + C)\) 也必然为一组解。


P1993 小K的农场

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
const int inf=INT_MAX;
typedef long long ll;
typedef pair<int,int> pii;

struct edge{int v,w;};
vector<edge>e[N];

int n,m;
long long d[N];
int cnt[N];
bool inq[N];

bool spfa(int s)
{
    memset(d,0x3f,sizeof(d));
    memset(cnt,0,sizeof(cnt));
    memset(inq,0,sizeof(inq));

    queue<int>q;
    d[s]=0; inq[s]=1; q.push(s);

    while(q.size())
    {
        int u=q.front(); q.pop();
        inq[u]=0;

        for(auto x:e[u])
        {
            int v=x.v,w=x.w;
            if(d[v] > d[u] + w)  // [新增] 松弛
            {
                d[v] = d[u] + w;
                cnt[v] = cnt[u] + 1;
                if(cnt[v] >= n+1) return 1;  // [新增] 加了超级源点,所以是 n+1
                if(inq[v]==0) inq[v]=1, q.push(v);
            }
        }
    }
    return 0;
}

int main()
{
    #ifndef ONLINE_JUDGE
        freopen("c.in","r",stdin);
        freopen("c.out","w",stdout);
    #endif
    // BEGIN!
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int op,a,b,c;
        cin>>op;
        // [新增] op=1: a-b >= c => b <= a-c,建边 a -> b,边权 -c
        if(op==1) cin>>a>>b>>c, e[a].push_back({b, -c});
        // [新增] op=2: a-b <= c => a <= b+c,建边 b -> a,边权 c
        else if(op==2) cin>>a>>b>>c, e[b].push_back({a, c});
        // [新增] op=3: a == b => a-b >= 0 且 a-b <= 0,建双向边
        else if(op==3) cin>>a>>b, e[a].push_back({b, 0}), e[b].push_back({a, 0});
    }
    for(int i=1;i<=n;i++) e[0].push_back({i,0});  // [新增] 超级源点

    if(spfa(0)) cout<<"No"<<endl;  // [新增] 有负环则无解
    else cout<<"Yes"<<endl;

    return 0;
}
posted @ 2026-08-01 20:27  敛才有道  阅读(6)  评论(0)    收藏  举报