图论-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的农场
也是数学上的变换:
- 当 \(a - b \ge C\),即 \(b \le a - C\),建边 \(a \to b\),边权为 \(-C\)
- 当 \(a - b \le C\),即 \(a \le b + C\),建边 \(b \to a\),边权为 \(C\)
- 当 \(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;
}

浙公网安备 33010602011771号