昨天的模拟赛中有一道用到最短路算法的题,自己竟然写T了,所以今天来温习一下三个最短路算法,把模板写一写。
首先说明,这三个算法都是无向图有向图皆适用的。
Floyd算法: 三个里面最好写的算法,算法原理是通过枚举中间点k,不断对两点之间的最短路长度进行松弛。d[i][j]表示i到j之间的最短路长度,d[i][j] = min{d[i][k] + d[k][j] | k ∈ [1, n]}。最终算法可以求出任意两点之间的最短路。由松弛表达式也可以看出,此算法要求保存图的方式是邻接矩阵:d[i][j]表示i到j之间一条边的长度(无边则为无穷大)。算法复杂度为O(n^3)。
模板:
void Floyd(){
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
值得注意的是:最外层循环是枚举中间点k,否则会出错,可以手推模拟一下证明。
Floyd传递闭包是一个很常见的用法,传递闭包只是用Floyd传递一种关系,比如连通性,可达关系。
[例题:POJ 3660](http://poj.org/problem?id=3660)
题意:给定n,m,表示有n头奶牛以及接下来输入的m个胜负关系,每组胜负关系形式为a,b,表示a可以战胜b,问有多少头奶牛的名次是可以确定的。
思路:当已知一个奶牛被i头战胜,又战胜了j头奶牛且i+j == n-1时,它的名次就是可以确定的,我们只需要知道这头奶牛是否被另一头奶牛战胜或是否战胜另一头奶牛即可。我们可以把每一个胜负关系看作有向边,使用Floyd传递闭包即可。
#include <cstdio>
int n, m, ans, sure;
bool d[101][101];
int main()
{
scanf("%d %d", &n, &m);
for(int a, b, i(1); i <= m; i++){
scanf("%d %d", &a, &b);
d[a][b] = 1;
}
for(int k = 1; k <= n; k++)
for(int i = 1; i <= n; i++)
for(int j = 1; j <= n; j++)
if(d[i][k] && d[k][j]) d[i][j] = 1;
for(int i = 1; i <= n; i++){
sure = 0;
for(int j = 1; j <= n; j++){
if(d[j][i]) sure++;
if(d[i][j]) sure++;
}
ans += sure == n-1;
}
printf("%d", ans);
}
不对不对,说好的温习一下模板呢,竟然写起了水题。算了,又不忍心删掉,就让它留着吧。
SPFA算法:这个算法是BellmanFord算法的优化版,但是优化的几乎面目全非,不过还是因为它比较好写,而且不难理解,就不提BellmanFord了。
算法用到队列,而且与Floyd不同的是,它求的是单源最短路,是一个起点到其他所有点的最短路,用d[i]表示i到起点的最短路长度。做法是,不断更新d数组,初值为正无穷,起点为0。从起点开始BFS,每次从队列中取出队首,查看它的所有边,用它的边去更新其他点,每当另一个点的d值被更新,那么把它入队,直到队列为空算法结束。
每个点的入队次数是有限的,因为它最多被其他每个点都更新一次,所以说一个点入队次数最多是n-1次。那么这个可以判断什么?——负环,可以说当图中有负环时SPFA是跑不出结果的,也可以说SPFA可以用来判断负环,解决方式就是记录每个点的入队次数。
针对于算法原理,用邻接表存图更好,这里我习惯用vector式的邻接表。
vector <int> G[M];
struct edge{
int u, v, w;
}E[M*M];
bool SPFA(){
memset(d, 0x7f, sizeof d);
d[s] = 0;
q.push(s);
times[s] = inq[s] = 1;
while(q.size()){
int u = q.front();
q.pop();
inq[u] = 0;
for(int i = 0; i < G[i].size(); i++){
int e = G[u][i], v = E[e].v, w = E[e].w;
if(d[v] > d[u]+w){
d[v] = d[u]+w;
if(!inq[v]){
q.push(v);
inq[v] = 1;
times[v]++;
if(times[v] == n) return 0;
}
}
}
}
return 1;
}
关于时间复杂度,SPFA(Shortest Path Faster Algorithm)是西安交大段凡丁提出的,论文中的复杂度是
O(kE),k为平均入队次数,且k一般<=2,但也有人说他的证明是有问题的,SPFA最坏的复杂度是O(NE),然而我并不理解。我知道的是,SPFA在完全图中会很吃力,如果是稀疏图,SPFA的复杂度是很好的。
可以看看这个:某位神犇对SPFA的详细分析
http://blog.csdn.net/xiazdong/article/details/8193680
Dijkstra算法:
在我的印象中,SPFA比Dijkstra要快一些,但稠密图的话,还是选用Dijkstra比较安全。但是,Dijkstra有一个很大的限制——不能用于有负权的图。
我们先看看算法的思路:
它与SPFA相同,都是求单源最短路的算法,仍然用d数组。我们设出两个集合:S集合,已求出最短路的点的集合,T集合,未求出最短路的点的集合,初始全在T集合中,d数组初值除了起点为0其他为正无穷。重复选择T集合中d值最小的点加入到S集合中,并用这个点去更新其他点的d值,直到所有点都加入到了S集合中,就得到了正确答案。这个过程与求生成树的prim算法是很像的。
Dijkstra算法既然有一个选取最小d值的点的过程,那么我们就可以在这个上面做出优化——堆优化。时间复杂度由O(N^2+M)降低至O((N+M)logN)
下面给出的模板就是堆优化的Dijkstra算法,存图方式与上一个SPFA相同。
struct node{
int d, num;
bool operator < (node i)const{
return d > i.d;
}
}t;
priority_queue <node> q;
void Dijkstra(){
memset(d, 0x7f, sizeof d);
d[s] = 0;
t.num = s, t.d = 0;
q.push(t);
while(q.size()){
t = q.top(); q.pop();
int u = t.num;
if(vis[u]) continue;
vis[u] = 1;
for(int j = 0; j < G[u].size(); j++){
int e = G[u][j], v = E[e].v, w = E[e].w;
if(d[v] > d[u]+w) {
d[v] = d[u] + w;
t.num = v, t.d = d[v];
q.push(t);
}
}
}
}