图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。(摘自百度百科)
1.Floyd 弗洛伊德算法
这种算法解决的是多源最短路问题(求任意两点之间的最短路径)
若我们用二维数组e[i][j]表示点i到点j当前的最短距离(程序运行完后数组中存的就是真正的最短距离)
那么我们可以用e[i][j]=max(e[i][j],e[i][k],e[j][k]);来更新e数组。也就是比较 从i到j 和 从i到k+从k到j 的距离
重点来啦!!!
核心思想:能否找到第三点使得任意两点的距离变短,即能否找到 i->k->j 比 i->j 短,如果能找到,就更新。
下面呈上代码:
//多元最短路 Floyd O(n^3) #include<iostream> #include<cstdlib> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; const int maxn=99999999; int n,m,e[1005][1005]; int main() { int i,j,k,a,b,c; scanf("%d%d",&n,&m); for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { if(i==j) e[i][j]; else e[i][j]=maxn; } } for(i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); e[a][b]=c; } //Floyd核心部分 for(i=1;i<=n;i++) for(j=1;j<=n;j++) for(k=1;k<=n;k++) if(e[j][k]>e[j][i]+e[i][k]) e[j][k]=e[j][i]+e[i][k]; for(i=1;i<=n;i++) { for(j=1;j<=n;j++) printf("%d ",e[i][j]); printf(" "); } return 0; }
很容易发现Floyd算法的时间复杂度是O(N^3)。这个复杂度很高,时间限制为1000ms的情况下,n=1000左右就不行了。
但它的优点是好写,好理解,可以解决负边权。
总结一下Floyd:
时间复杂度:O(N^3)
优点:好写,好理解,可以解决负边权
缺点:太慢
2.Dijkstra 迪杰斯特拉
这种算法解决的是单源最短路问题(求任意一点与其它点之间的最短路径)
这种算法也要用e数组,作用同上
本算法还要用dis数组,我们用dis[i]表示点i到源点当前的最短距离,这个值是不断变化的
个人感觉Dijkstra比Floyd要难一点
方法:
1.我们可以把所有的点分为两个集合,集合p和集合q,用数组book表示。book[i]==1的为p表示选过的点,book[j]==0为q表示没选过的点.;
2.从q集合中选择一个离源点最近的点,即 使dis[u]最小,将该点u加入p集合。并搜索每一条以u为起点的边进行松弛,即比较dis[v]和dis[u]+e[u][v];
3.重复第2步,直到q集合为空。此时dis数组中存的就是最终结果。
下面呈上代码:(默认源点为1号顶点)
#include<iostream> #include<cstdlib> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; const int maxn=99999999; int n,m,e[1005][1005],dis[1005],book[1005]; int main() { int i,j,a,b,c; scanf("%d%d",&n,&m); for(i=1;i<=n;i++) { for(j=1;j<=n;j++) { if(i==j) e[i][j]; else e[i][j]=maxn; } } book[1]=1; for(i=1;i<=m;i++) { scanf("%d%d%d",&a,&b,&c); e[a][b]=c; } for(i=1;i<=n;i++) dis[i]=e[1][i]; for(i=1;i<=n-1;i++) { int minn=maxn,u; for(j=1;j<=n;j++) { if(dis[j]<minn && book[j]==0) { minn=dis[j]; u=j; } } book[u]=1; for(j=1;j<=n;j++) { if(e[u][j]<maxn) { if(dis[j]>dis[u]+e[u][j]) dis[j]=dis[u]+e[u][j]; } } } for(i=1;i<=n;i++) printf("%d ",dis[i]); return 0; }
Dijkstra的时间和空间复杂度都是O(n^2)
此外,我们发现Dijkstra在找到u点后需要把图扫一遍,所以可以用邻接表来优化
下面呈上Dijkstra邻接表优化的代码:
#include<iostream> #include<cstdlib> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; const int maxn=99999999; int n,m,dis[1005],book[1005]; int b[1005],c[1005]; int first[1005],next[1005]; int main() { int i,j,x,y,z; scanf("%d%d",&n,&m); //book[1]=1; for(i=1;i<=m;i++)//建立邻接表 { scanf("%d%d%d",&x,&y,&z); b[i]=y; c[i]=z; next[i]=first[x];//建表的关键 first[x]=i; } memset(dis,88,sizeof(dis)); dis[1]=0; //Dijkstra的核心部分 for(i=1;i<=n-1;i++) { int minn=maxn,u; for(j=1;j<=n;j++) { if(dis[j]<minn && book[j]==0) { minn=dis[j]; u=j; } } book[u]=1; for(j=first[u];j;j=next[j]) { dis[b[j]]=min(dis[b[j]],dis[u]+c[j]); } } for(i=1;i<=n;i++) printf("%d ",dis[i]); return 0; }
优化后,时间变成了O((M+N) log N),空间降到了O(M)。一般的,m会比n^2小很多,所以优化后总体较优。
总结一下Dijkstra:
时间复杂度:O((M+N) log N)
优点:时间空间复杂度都较低,思路值得借鉴
缺点:不能解决负边权
3.Bellman-Ford 贝尔曼福德算法
个人最喜欢的最短路算法(同感的点个赞呗)
这种算法解决的也是单源最短路问题。
这种算法也需要dis数组,作用同上。
方法:
1.枚举每一条边,比如说u->v的边,看看能否通过该边使得dis[v]变短,即比较dis[v]和dis[u]+c[i](c为路径长度)
2.重复步骤1,每重复一遍称为一遍松弛
那么需要松弛多少次呢?
n-1次。因为任意两点之间的路径最多经过n-1条边。
敲黑板划重点:
最短路径中不可能包含回路!
讲完了,呈上代码:
#include<iostream> #include<cstdlib> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int n,m,a[10005],b[10005],c[10005],dis[10005]; int main() { int i,j; memset(dis,88,sizeof(dis)); dis[1]=0; scanf("%d%d",&n,&m); for(i=1;i<=m;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]); for(i=1;i<=n-1;i++) { for(j=1;j<=m;j++) { if(dis[b[j]]>dis[a[j]]+c[j]) { dis[b[j]]=dis[a[j]]+c[j]; } } } for(i=1;i<=n;i++) printf("%d ",dis[i]); return 0; } Bellman-Ford
此外,Bellman-Ford算法还能判断一个图有没有负权回路,如果松弛完了之后仍存在
dis[b[j]]>dis[a[j]]+c[j]
则此图有负权回路,因为从负权回路走一圈可以使最短路径再次变短。
有人可能会问了:真的必须要松弛n-1次吗?
答案是:不需要!n-1次只是一个最大值罢了。如果当前的一轮松弛没有作用,那么后面的都不会起作用,此时可以提前跳出循环
我们将上面几行的描述加入到代码中,如下:
#include<iostream> #include<cstdlib> #include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int n,m,a[10005],b[10005],c[10005],dis[10005]; int main() { int i,j,check; memset(dis,88,sizeof(dis)); dis[1]=0; scanf("%d%d",&n,&m); for(i=1;i<=m;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]); for(i=1;i<=n-1;i++) { check=0; for(j=1;j<=m;j++) { if(dis[b[j]]>dis[a[j]]+c[j]) { dis[b[j]]=dis[a[j]]+c[j]; check=1; } } if(check==0) break; } for(j=1;j<=m;j++) if(dis[b[j]]>dis[a[j]]+c[j]) { printf("此图有负权回路"); return 0; } for(i=1;i<=n;i++) printf("%d ",dis[i]); return 0; }
此外,Bellman-Ford算法还有一种队列优化方法:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。
我们可以用一个队列来维护这些点,用邻接表来储存图
下面呈上代码:
#include<iostream> #include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> #include<cmath> #include<queue> using namespace std; int n,m,a[1005],b[1005],c[1005],dis[1005],book[1005],first[1005],next[1005]; int main() { int i,x,y,z; queue <int> Q; scanf("%d%d",&n,&m); for(i=1;i<=m;i++) { scanf("%d%d%d",&x,&y,&z); a[i]=x; b[i]=y; c[i]=z; next[i]=first[x]; first[x]=i; } memset(dis,88,sizeof(dis)); dis[1]=0; Q.push(1); book[1]=1; while(!Q.empty()) { for(i=first[Q.front()];i;i=next[i]) { if(dis[b[i]]>dis[a[i]]+c[i]) { dis[b[i]]=dis[a[i]]+c[i]; if(book[b[i]]==0) { Q.push(b[i]); book[b[i]]=1; } } } book[Q.front()]=0; Q.pop(); } for(i=1;i<=n;i++) printf("%d ",dis[i]); return 0; } Bellman-Ford队列优化
总结一下Bellman-Ford
时间复杂度:最坏是O(MN)
优点:可以解决负边权,空间占用小
缺点:再快一点就完美了
4.总结
Floyd |
Dijkstra |
Bellman-Ford |
|
空间复杂度 |
O(N^2) |
O(M) |
O(M) |
时间复杂度 |
O(N^3) |
O((M+N) log N) |
最坏是O(MN) |
单源or多源最短路 |
多源 |
单源 |
单源 |
其它 |
可以解决负边权 |
不能解决负边权 |
可以解决负边权 |
每种算法都各有所长,我们要根据实际情况,选择最合适的算法。
~祝大家编程顺利~