特别说明
本文转载自三金(frinemore)的博客: 点这
前言
1.关于SPFA,它没死。
2.接下来的所有代码,都是自己手写的(未检查正确性,补充的代码有检查过,是对的),有错误请帮忙指出。
SPFA原理及正确性
若给定的图存在负权边,类似Dijkstra算法等算法便没有了用武之地,SPFA算法便派上用场了。简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
定理:只要最短路径存在,上述SPFA算法必定能求出最小值。证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。
其实可以这样理解:
在给定一个不存在负权回路的图中,有n个节点,m条边
那么在求某点(假设此点为1)单源最短路时,到最远点的距离最多经历n-1条边。
那么用m条边进行n-1次松弛就可以求出最短路了。
下面是Bellman——Ford Algorithm:
const int MAXNC = 10000;
struct edge {
int x, y, val;
}a[MAXNC];
for(int i = 1; i < n; i++)
for(int j = 1; j <= m; j++) {
int x = a[j].x, y = a[j].y;
if(d[y] > d[x] + a[j].val) d[y] = d[x] + a[j].val;
}
判负环:
bool have_negetive_circle = false;
for(int i = 1; i<= m; i++) {
int x =a[i].x, y = a[i].y;
if(d[y] > d[x] + a[i].val) {
have_negetive_circle = true;
break;
}
}
if(have_negetive_circle) printf("...");
优化
由于传统算法时间复杂度很高(o(n * m)),所以得优化一下。
宽搜优化
因为只有d值被更新的结点的出度才能更新其他节点的d值,所以我们可以用队列(queue)优化。
这是代码:
void SPFA(int s) {
queue<int> Q;
for(int i = 1; i <= n; i++) d[i] = INF;
d[s] = 0;
Q.push(s);
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = false;
for(int i = 1; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
if(!inq[y]) Q.push(y);
}
}
}
}
不过20行代码。
SPFA队列判负环
只需要在入队是记录次数就好了,如果大于了n,说明有负环。
code:
bool SPFA(int s) {
queue<int> Q;
for(int i = 1; i <= n; i++) d[i] = INF, cnt[i] = 1;
d[s] = 0; cnt[s] = 1;
Q.push(s);
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = false;
for(int i = 1; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
if(!inq[y]) {
Q.push(y);
inq[y] = true;
if(++cnt[y] > n) return true;
}
}
}
}
return false;
}
其实就在原有代码上改了一点,但这有个缺点,就是只能找从源点s出发的负环,如果s不能到达负环,那么这个是检查不出来的。
这个问题当然是有办法解决的,下面是代码。
code:
bool have_negative_circle() {
queue<int> Q;
for(int i = 1; i <= n; i++) d[i] = 0, cnt[i] = 1, inq[i] = true, Q.push(i);
//d[i] = 0,这样更容易找负环,效率更高
while(!Q.empty()) {
int x = Q.front(); Q.pop();
for(int i = head[x]; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
if(!inq[y]) {
inq[y] =true;
Q.push(y);
if(++cnt[y] >= n) return true;
}
}
}
}
return false;
}
这个代码的效率要比上面的高多了,但只能找负环。
深搜优化:
我也只是知道有这个优化,但具体不清楚,不过我旁边的那位大神犇有仔细研究过,让他来讲。
基于 dfs 版的 SPFA 相当于是把"先进先出"的队列换成了"先进后出"的栈
也就是说,每次都以刚刚松弛过的点来松弛其他的点,如果能够松弛点 x 并且 x 还在栈中,那图中就有负环
一般来说的话,若存在负环,那么 dfs 会比 bfs 快
但是如果不存在负环,dfs 可能会严重影响求最短路的效率,要谨慎使用
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 10005
#define M 20005
using namespace std;
int n,m,t;
int d[N],first[N];
int v[M],w[M],next[M];
bool flag,vis[N];
void add(int x,int y,int z)
{
t++;
next[t]=first[x];
first[x]=t;
v[t]=y;
w[t]=z;
}
void spfa(int x)
{
int i,j;
vis[x]=true;
for(i=first[x];i;i=next[i])
{
j=v[i];
if(d[j]>d[x]+w[i])
{
if(vis[j])
{
flag=false;
return;
}
d[j]=d[x]+w[i];
spfa(j);
}
}
vis[x]=false;
}
int main()
{
int x,y,z,i;
scanf("%d%d",&n,&m);
for(i=1;i<=m;++i)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
memset(d,127,sizeof(d));
d[1]=0;
flag=true;
spfa(1);
if(!flag) printf("Yes");
else printf("No");
return 0;
}
深搜代码:作者:forever_dreams
原文:https://blog.csdn.net/forever_dreams/article/details/81161527
应用
下面介绍我所知道的SPFA应用。
求最短路
如果没有负权边,那么请用效率更高的Dijkstra,SPFA在处理极限数据的时候会退化。
code:
void SPFA(int s) {
queue<int> Q;
for(int i = 1; i <= n; i++) d[i] = INF;
d[s] = 0;
Q.push(s);
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = false;
for(int i = 1; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
if(!inq[y]) Q.push(y);
}
}
}
}
求最长路
虽然大部分无环图都可以转化为DAG经典模型求解,但SPFA也是可以的,dijkstra好像也行(这个不太清楚)。
代码在后面例题讲。
判负环
这好像是SPFA的标志了,在差分约束有用到。
例题
虽然说这题是动态规划的例题,但也可以最长路做。
题目是这个意思:
有n(n <= 30) 种立方体,每个都有无穷个。要求选择一些立方体垒成一根尽量高的柱子(可以自行选择长宽高),使得每个立方体的底面长宽高分别严格小于它下方的立方体的底面长宽。
思路:
每个立方体说是可以用无穷次,其实最多只能用三次。然后每次输入一个立方体的时候转化为三个立方体,
O(n^2)建边,如果立方体x底面长宽 小于立方体y底面长宽就加一条y->x 的边,权值为立方体x的高度。
这样还不够,我们再用一个多余的节点对每一个立方体加边,权值为该立方体的高,然后跑最长路就好了。
code:(样例都没过,所以就看看原理)
#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int MAXNC = 100, MAXN = 1000;
const long long INF = 1e11 + 9;
struct point {
int x, y, z;
point() {}
point(int x, int y, int z) {
this->x = x;
this->y = y;
this->z = z;
}
}a[MAXNC];
struct edge{
int y, next, val;
edge() {}
edge(int y, int next, int val) {
this->y = y;
this->next = next;
this->val = val;
}
}e[MAXN];
int n, kase, ans, cnt;
int head[MAXNC];
long long
d[MAXNC];
bool inq[MAXNC];
bool judge(point x, point y) {
if(x.x > y.x && x.y > y.y) return true;
else if(x.y > y.x && x.x > y.y) return true;
else return false;
}
void add_edge(int x,int y,int val) {
e[++cnt] = edge(y, head[x], val);
head[x] = cnt;
return;
}
void SPFA(int s) {
queue<int> Q;
for(int i = 1; i <= 3 * n; i++) d[i] = 0;
// d[s] = 0;
Q.push(s);
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = false;
for(int i = head[x]; i; i = e[i].next) {
// printf("%d %d %d
", x, e[i].y, e[i].val);
int y = e[i].y;
if(d[y] < d[x] + e[i].val) {
d[y] = d[x] + e[i].val;
if(!inq[y]) Q.push(y), inq[y] = true;
}
}
}
}
int main() {
scanf("%d",&n);
while(n) {
for(int i = 1; i < 3 *n; i += 3) {
int x, y, z;
scanf("%d %d %d", &x, &y, &z);
a[i] = point(x, y, z);
a[i + 1] = point(y, z, x);
a[i + 2] = point(z, x, y);
}
memset(head,0,sizeof(head));
cnt = 0;
for(int i = 1; i<= 3 * n; i++)
for(int j = 1; j <= 3 *n; j++)
if(i != j && judge(a[i], a[j]))
add_edge(i, j, a[j].z);
for(int i = 1; i <= 3 * n; i++)
add_edge(3 * n + 1, i, a[i].z);
SPFA(3 * n + 1);
ans = 0;
for(int i = 1; i<= 3 * n; i++) if(d[i] > ans) ans = d[i];
// for(int i = 1; i <= 3 * n; i++) printf("%d ",-d[i]);
// for(int i = 1; i <= 3 * n + 1; i++)
// for(int j = head[i]; j; j = e[j].next)
// printf("x=%d y=%d val=%d
", i, e[j].y, e[j].val);
printf("Case %d: maximum height = %d
", ++kase, ans);
scanf("%d",&n);
}
return 0;
}
下面是AC代码(题解最长路):
#include<iostream>
#include<cstring>
#include<queue>
#include<vector>
using namespace std;
int n, mx;
int dis[100], visited[100];
queue<int> q;
struct cubes{
int x;
int y;
int z;
}cube[100];
struct Node{
int from;
int to;
int weight;
};
vector<Node> edges[100];
void spfa(){
while(!q.empty()){
int top=q.front(); q.pop();
visited[top]=false;
for(int i=0;i<edges[top].size();i++){
int t=edges[top][i].to;
if(dis[t]<edges[top][i].weight+dis[top]){
dis[t]=edges[top][i].weight+dis[top];
if(!visited[t]){
visited[t]=true;
q.push(t);
}
}
}
}
}
int main(){
cin >> n;
int c=0;
while(n!=0){
c++;
memset(cube, 0, sizeof(cube));
memset(dis, 0, sizeof(dis));
for(int i=0;i<100;i++){
edges[i].clear();
}
for(int i=1;i<=3*n;i+=3){
int x, y, z;
cin >> x >> y >> z;
cube[i].x=x; cube[i].y=y; cube[i].z=z;
cube[i+1].x=y; cube[i+1].y=z; cube[i+1].z=x;
cube[i+2].x=z; cube[i+2].y=x; cube[i+2].z=y;
}
for(int i=1;i<=n*3;i++){
for(int j=1;j<=n*3;j++){
if((cube[i].x<cube[j].x && cube[i].y<cube[j].y) || (cube[i].x<cube[j].y && cube[i].y<cube[j].x)){
// cout << j << " to " << i << ": " << cube[j].x << " " << cube[i].x << " " << cube[j].y << " " << cube[i].y << endl;
Node a;
a.from=j;
a.to=i;
a.weight=cube[i].z;
edges[j].push_back(a);
}
}
}
for(int i=1;i<=3*n;i++){
dis[i]=cube[i].z;
q.push(i);
visited[i]=true;
}
spfa();
int mx=0;
for(int i=1;i<=n*3;i++){
if(dis[i]>mx) mx=dis[i];
}
cout << "Case " << c << ": maximum hight = ";
cout << mx << endl;
cin >> n;
}
return 0;
}
(写的还没我的好看)
2.P3385 【模板】负环
都说是模板了,下面是代码:
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int MAXNC=2000+100,MAXN=500000,INF=0x7fffffff;
int d[MAXNC],head[MAXNC],num[MAXNC];
bool inq[MAXNC];
int n,m,cnt;
struct edge {
int y,next,val;
edge() {}
edge(int y,int next,int val) {
this->y=y;
this->next=next;
this->val=val;
}
}a[MAXN];
void add_edge (int x,int y,int val);
bool SPFA(int s) {
queue<int> Q;
for(int i = 1;i <= n;i++) d[i] = INF,num[i]=0,inq[i]=0;
Q.push(s);
d[s] = 0; inq[s] = 1;
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = 0;
if(num[x] >= n) return true;
for(int i = head[x]; i; i = a[i].next) {
int y=a[i].y;
if(d[y]> d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
if(!inq[y]) {
Q.push(y);
inq[y]=1;
if(++num[y]>=n) return true;
}
}
}
}
return false;
}
int main(){
int t;
scanf("%d",&t);
while(t--){
scanf("%d %d", &n, &m);
cnt=0;
memset(head,0,sizeof(head));
for(int i = 1;i <= m; i++) {
int x,y,val;
scanf("%d %d %d", &x, &y, &val);
if(val<0) add_edge(x,y,val);
else add_edge(x,y,val),add_edge(y,x,val);
}
if(SPFA(1)) printf("YE5
");
else printf("N0
");
}
return 0;
}
void add_edge(int x,int y,int val){
a[++cnt] = edge(y, head[x], val);
head[x] = cnt;
return ;
}
请仔细看看“YE5” 和 “N0”
最短路的例题大都可以用Dijkstra做,而且效率更高,这就不再说最短路的题了。
总结
SPFA求最短路的时间复杂度是o(KE)的,k是一个常数,一般为2,但处理特殊数据时会“退化”。(Dijkstra不会);
SPFA可以处理负边权而且可以找负环(Dijkstra不行);
SPFA的算法原理(渐进法)使得它作用范围更广(BFS);
而且代码量要比Dijkstra要少很多。
一个非常优秀的算法。
补充
链式前向星记录路径
我没不知道说什么,就是再求最短路的时候记录当前松弛的边,这必然是在最短路径上的边,然后一点一点摸到起点上去就好。
SPFA版:
#include<cstdio>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXNC = 1e5 + 100, MAXN = 1e6, INF = 0x3f3f3f3f;
struct edge {
int x, y, next, val, cnt; // 为了记录路径竟然多塞了这么多东西
edge() {}
edge(int x, int y, int next, int val, int cnt) {
this->x = x;
this->y = y;
this->next = next;
this->val = val;
this->cnt = cnt;
}
}a[MAXN];
vector<int> Path[MAXNC];
int head[MAXNC], d[MAXNC], p[MAXNC],inq[MAXNC];
int n, m, cnt, S;
void read();
void add_edge(int x, int y, int val);
void SPFA(int s);
void Get_Shortest_Paths(int s, vector<int>* Path);
void ask_print();
int main() {
scanf("%d %d %d", &n, &m, &S);
read();
Get_Shortest_Paths(S, Path); //真是毒瘤
ask_print();
return 0;
}
void read() {
for(int i = 1; i <= m; i++) {
int x, y, val;
scanf("%d %d %d", &x, &y, &val);
add_edge(x, y, val);
}
return;
}
void add_edge(int x, int y, int val) {
a[++cnt] = edge(x, y, head[x], val, cnt);
head[x] = cnt;
return;
}
void SPFA(int s) {
queue<int> Q;
for(int i = 1; i <= n; i++) d[i] = INF;
d[s] = 0;
Q.push(s);
while(!Q.empty()) {
int x = Q.front(); Q.pop();
inq[x] = false;
for(int i = head[x]; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
p[y] = a[i].cnt;
if(!inq[y] && (inq[y] = true)) Q.push(y);
}
}
}
return;
}
void Get_Shortest_Paths(int s, vector<int>* Path) {
SPFA(s);
for(int i = 1; i <= n; i++) {
Path[i].clear();
int t = i;
Path[i].push_back(t);
while(s != t) {
Path[i].push_back(a[p[t]].x);
t = a[p[t]].x;
}
reverse(Path[i].begin(),Path[i].end());
}
return;
}
void ask_print() {
int x;
scanf("%d", &x);
for(vector<int> :: iterator it = Path[x].begin(); it != Path[x].end(); it++)
printf("%d ", *it);
// printf("
d[%d] = %d",x,d[x]);
printf("
");
return;
}
/*
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
4
*/
Dijkstra版:
#include<cstdio>
#include<queue>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXNC = 1e6, MAXN = 1e4, INF = 0x3f3f3f3f;
struct heapnode {
int x, s;
heapnode() {}
heapnode(int x,int s) {
this->x = x;
this->s = s;
}
bool operator < (const heapnode &rhs) const {
return s > rhs.s;
}
};
struct edge{
int x, y, next, val, cnt;
edge() {}
edge(int x, int y, int next, int val, int cnt) {
this->x = x;
this->y = y;
this->next = next;
this->val = val;
this->cnt = cnt;
}
}a[MAXNC];
vector<int> Path[MAXN];
int d[MAXN], vis[MAXN], head[MAXN], p[MAXN];
int n, m, cnt, S;
void read();
void add_edge(int x, int y, int val);
void Dijkstra(int s);
void Get_shortest_Path(int s);
void ask_print();
int main() {
scanf("%d %d %d", &n, &m, &S);
read();
Get_shortest_Path(S);
ask_print();
return 0;
}
void read() {
for(int i = 1; i <= m; i++) {
int x, y, val;
scanf("%d %d %d", &x, &y, &val);
add_edge(x, y, val);
}
}
void add_edge(int x, int y, int val) {
a[++cnt] = edge(x, y, head[x], val, cnt);
head[x] = cnt;
}
void Get_shortest_Path(int s) {
Dijkstra(s);
for(int i = 1; i <= n; i++) {
Path[i].clear();
int t = i;
Path[i].push_back(i);
while(t != s) {
Path[i].push_back(a[p[t]].x);
t = a[p[t]].x;
}
reverse(Path[i].begin(), Path[i].end());
}
return;
}
void Dijkstra(int s) {
priority_queue<heapnode> Q;
for(int i = 1; i <= n; i++) d[i] = INF;
d[s] = 0;
Q.push(heapnode(s,0));
while(!Q.empty()) {
heapnode tmp = Q.top(); Q.pop();
int x = tmp.x;
if(vis[x]) continue;
vis[x] = true;
for(int i = head[x]; i; i = a[i].next) {
int y = a[i].y;
if(d[y] > d[x] + a[i].val) {
d[y] = d[x] + a[i].val;
p[y] = a[i].cnt;
Q.push(heapnode(y,d[y]));
}
}
}
return;
}
void ask_print() {
int x;
scanf("%d", &x);
for(vector<int> :: iterator it = Path[x].begin();it != Path[x].end(); it++)
printf("%d ", *it);
printf("
");
}
路径追踪例题
两个题,我也都没做过。
1,UVA11374 Airport Express(省选难度,加油)
在IokhIok**h市中,机场快线是市民从市内去机场的首选交通工具。机场快线分为经济线和商业线两种,线路、速度和价钱都不同。你有一张商业线车票,可以坐一站商业线,而其他时候只能乘坐经济线。假设换乘时间忽略不计,你的任务是找一条去机场最快的线路。
输入格式:
输入包含多组数据。每组数据第一行为33个整数N, SN,S和E(2 leq N leq 500, 1 leq S, E leq 100)E(2≤N≤500,1≤S,E≤100),即机场快线中的车站总数,起点和终点(即机场所在站)编号。下一行包含一个整数M(1 leq M leq 1000)M(1≤M≤1000),即经济线的路段条数。以下MM行每行3个整数X, Y, Z(1 leq X, Y leq N, 1 leq Z leq 100)X,Y,Z(1≤X,Y≤N,1≤Z≤100),表示可以乘坐经济线在车站XX和车站YY之间往返,其中单程需要ZZ分钟。下一行为商业线的路段条数K(1 leq K leq 1000)K(1≤K≤1000),以下KK行是这些路段的描述,格式同经济线。所有路段都是双向的,但有可能必须使用商业车票才能到达机场。保证最优解唯一。
输出格式:
对于每组数据,输出33行。第一行按访问顺序给出经过的各个车站(包括起点和终点),第二行是换乘商业线的车站编号(如果没有商业线车票,输出Ticket Not Used
),第三行是总时间。
2,P2176 [USACO14FEB]路障Roadblock(这道题个人认为比上面的难,但难度却是提高的)
题目描述
每天早晨,FJ从家中穿过农场走到牛棚。农场由 N 块农田组成,农田通过 M 条双向道路连接,每条路有一定长度。FJ 的房子在 1 号田,牛棚在 N 号田。没有两块田被多条道路连接,以适当的路径顺序总是能在农场任意一对田间行走。当FJ从一块田走到另一块时,总是以总路长最短的道路顺序来走。
FJ 的牛呢,总是不安好心,决定干扰他每天早晨的计划。它们在 M 条路的某一条上安放一叠稻草堆,使这条路的长度加倍。牛希望选择一条路干扰使得FJ 从家到牛棚的路长增加最多。它们请你设计并告诉它们最大增量是多少。
输入输出格式
输入格式:
第 1 行:两个整数 N, M。
第 2 到 M+1 行:第 i+1 行包含三个整数 A_i, B_i, L_i,A_i 和 B_i 表示道路 i 连接的田的编号,L_i 表示路长。
输出格式:
第 1 行:一个整数,表示通过使某条路加倍而得到的最大增量。
输入输出样例
输入样例#1:
5 7 2 1 5 1 3 1 3 2 8 3 5 7 3 4 3 2 4 7 4 5 2
输出样例#1:
2