很久没有练习最短路径的题目了,本以为掌握的不错,却忽略了每种算法都有它的拓展,它的变式,并非所有的题目都是让你求个最短路径那么简单(即使在一般的比赛中spfa也仅仅是用来求最短路径)。刷了几天的水题,也是时候做些总结了。
众所周知,最短路径的算法一共有四种,分别是(ASAP)Floyed,(SSSP)Dijkstra,(SSSP)BellmanFord,(SSSP)SPFA。个人认为SPFA仅仅是BellmanFord的强化版,应该归属于同一种算法。因此,以下只总结前三种算法。
Floyed算法:
Floyed算法较为简单,本质就是一个DP来求多源最短路,由于代码简单,常被用于距离的预处理中。不多说,先上标准代码
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int MAXN = 101;
int n,m,s,t;
int map[MAXN][MAXN];
int main()
{
memset(map,127,sizeof(map));//距离赋为无穷大
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int from,to,dist;
scanf("%d%d%d",&from,&to,&dist);
map[from][to] = dist;
}
scanf("%d%d",&s,&t);
//Floyed
for (int k=1;k<=n;k++) //先枚举中间节点
for (int i=1;i<=n;i++) if (i != k)
for (int j=1;j<=n;j++) if (j != k && j != i)
map[i][j] = min(map[i][j],map[i][k]+map[k][j]);
printf("%d\n",map[s][t]);
return 0;
}
那么,Floyed算法还有没有其它的用途呢?当然有,首先的一个作用便是判断负权回路,实现很简单,只需要在最后判断map[i][i](0<i<=n)是否小于0,若是,则存在负权回路。但这种判回路的方法时间复杂度为O(n^3),效率远远比不上BellmanFord,因此不经常用。
除此之外,Floyed最小环算法被广泛地使用,如果你要解决这样一个问题:在一个有向图中,可从任意一顶点出发,求最小回路是多少?
虽然本题可以使用Dijkstra来完成,但没有Floyed方便快捷。Floyed最小环算法在网上有介绍,这里不作详解,仅贴上核心代码
//Floyed最小环
//map[i][j]表示原距离
for (int k=1;k<=n;k++) //先枚举中间节点
for (int i=1;i<=n;i++) if (i != k)
for (int j=1;j<=n;j++) if (j != k && j != i)
ans = min(ans,f[i][j]+map[i][k]+map[k][j]);
Dijkstra算法:
Dijkstra是以贪心为基础的算法,它的原理很好理解,每次都找与起点距离最小的点,再不断更新,直到所有点都被更新过为止。强烈建议使用邻接表来进行存储,因为这样既省空间又省时间,在vector的辅助下,存边已经不需要链表的辅助。
#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN = 10001;
const int INF = 1000001;
struct Node
{
int to,dist;
};
int n,m,s,t;
int dist[MAXN];
bool vis[MAXN];
vector <Node> G[MAXN];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int from,to,dist;
scanf("%d%d%d",&from,&to,&dist);
G[from].push_back((Node){to,dist});
}
scanf("%d%d",&s,&t);
memset(dist,127,sizeof(dist));
for (int i=0;i<G[s].size();i++) dist[G[s][i].to] = min(dist[G[s][i].to],G[s][i].dist);
dist[s] = 0; vis[s] = true;
for (int i=1;i<=n;i++)
{
int k,_min = INF;;
for (int j=1;j<=n;j++) if (!vis[j] && dist[j] < _min)
{
k = j;
_min = dist[j];
}
if (_min == INF) break;
vis[k] = true;
for (int j=0;j<G[k].size();j++)
if (!vis[G[k][i].to] && dist[G[k][i].to] > dist[k]+G[k][i].dist) dist[G[k][i].to] = dist[k]+G[k][i].dist;
}
printf("%d\n",dist[t]);
return 0;
}
从以上代码我们发现,Dijkstra算法的时间复杂度为O(n^2),而大部分时间都是用来查找最小边权。有什么数据结构可以来快速寻找最值呢?优先队列!实现方法较为简单,直接上代码
Heap Dijkstra:
#include <cstdio>
#include <cmath>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 10001;
struct Node
{
int to,dist;
bool operator < (const Node &x) const
{
return dist > x.dist;
}
};
int n,m,s,t;
int dist[MAXN];
bool done[MAXN];
vector <Node> G[MAXN];
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int from,to,dist;
scanf("%d%d%d",&from,&to,&dist);
G[from].push_back((Node){to,dist});
}
scanf("%d%d",&s,&t);
memset(dist,127,sizeof(dist));
priority_queue <Node> Q;Q.push((Node){s,0});
dist[s] = 0;
while (!Q.empty())
{
Node x = Q.top();Q.pop();
if (!done[x.to])
{
done[x.to] = true;
for (int i=0;i<G[x.to].size();i++)
if (dist[G[x.to][i].to] > dist[x.to] + G[x.to][i].dist)
{
dist[G[x.to][i].to] = dist[x.to] + G[x.to][i].dist;
Q.push((Node){G[x.to][i].to,dist[G[x.to][i].to]});
}
}
}
printf("%d\n",dist[t]);
return 0;
}
从上面这段代码我们可以发现,优先队列维护的Dijkstra的时间复杂度为O(n*logn),是一个稳定的优秀算法。正因为它的稳定性,它的作用甚至比SPFA还大——除了不能处理负权。在许多比赛中,正因为SPFA有着不稳定性,常常会被出题人卡数据,导致SPFA降为O(VE)的算法,因此
强烈建议在竞赛中使用Heap Dijkstra。
BellmanFord算法:
在许多比赛中,边权常常有负值——甚至会出现负权回路。因此,最稳定的Heap Dijkstra便无法发挥出它的作用。于是,BellmanFord算法便展现出了它的威力。O(VE)的算法,可以很好的处理负权并有效地判断负权回路。
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
const int MAXN=1001;
struct Node
{
int from,to,dist;
};
int n,m,s,t;
int dist[MAXN];
int cnt[MAXN];
vector <Node> G;
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int from,to,dist;
scanf("%d%d%d",&from,&to,&dist);
G.push_back((Node){from,to,dist});
}
scanf("%d%d",&s,&t);
memset(dist,127,sizeof(dist));
for (int i=0;i<m;i++) if (G[i].from == s) dist[G[i].to] = min(dist[G[i].to],G[i].dist);
dist[s] = 0;
for (int i=1;i<=n;i++)
{
bool flag = false;
for (int j=0;j<m;j++)
{
if (dist[G[j].to] > dist[G[j].from]+G[j].dist)
{
flag = true;
dist[G[j].to] = dist[G[j].from]+G[j].dist;
}
}
if (!flag) break;//避免冗余操作
}
bool Negative_Ctyle = false;
for (int i=0;i<m;i++) if (dist[G[i].to] > dist[G[i].from]+G[i].dist)
{
Negative_Ctyle = true;
break;
}
if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negetive Ctyle Exists!\n");
return 0;
}
由代码我们可以发现,O(VE)的操作中存在许多冗余操作,即使是加了flag来进行判断,也仅仅是去除了少部分冗余,在效率上并没有太大的提升。因此,为了有效地提升效率,SPFA算法便产生了。它使用队列来维护松弛操作,将时间复杂度降为O(kE)(可以证明k的平均大小为2),代码如下
SPFA:
#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int MAXN=1001;
const int INF=100000;
struct Node
{
int to,dist;
};
int n,m,s,t;
int dist[MAXN];//离源点的最短距离
int cnt[MAXN]; //判断是否存在负权回路
bool inq[MAXN];//是否在队列中
vector <Node> G[MAXN];
bool SPFA(int s)
{
for (int i=1;i<=n;i++) dist[i]=INF;
memset(inq,false,sizeof(inq));
dist[s]=0;inq[s]=true;
queue <int> Q;
Q.push(s);
while (!Q.empty())
{
int x=Q.front();Q.pop();
inq[x]=false;
for (int i=0;i<G[x].size();i++)
if (dist[x]+G[x][i].dist<dist[G[x][i].to])
{
dist[G[x][i].to]=dist[x]+G[x][i].dist;
if (!inq[G[x][i].to])
{
Q.push(G[x][i].to);
inq[G[x][i].to]=true;
if (++cnt[G[x][i].to] > n) return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int from,to,dist;
scanf("%d%d%d",&from,&to,&dist);
G[from].push_back((Node){to,dist});
}
scanf("%d%d",&s,&t);
bool Negative_Ctyle = SPFA(s);
if (!Negative_Ctyle) printf("%d\n",dist[t]);else printf("Negative Ctyle Exist!\n");
return 0;
}
以上代码还可以很好地实现差分约束系统,这里不作详解。
关于SPFA与BFS,这也是最近看书的时候看到的,其实很多BFS的题目都可以由SPFA解决,比如经典的迷宫问题,用SPFA效率会快很多。
后记:
为了更好地帮助新手实现模板,这里给出最短路径的模板题目
http://www.rqnoj.cn/problem/341
同时,推荐几道题,供大家学习。
https://www.vijos.org/p/1119(标准最短路,没什么难度但很累)
https://www.vijos.org/p/1046(Floyed最小环)
http://poj.org/problem?id=1860(BellmanFord判断负权回路)
https://www.vijos.org/p/1155(次小路)