总的来说有这些最短路算法:floyd,Dijkstra,Bellman,SPFA
的思想极其精炼,基于动态规划思想,代码极其简单
for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ if(dis[i][j]>dis[i][k]+dis[k][j]) dis[i][j]=dis[i][k]+dis[k][j]; }循环到k次dis[i][j]矩阵的意义是:从i到j只经过前k号点的所能达到的最短路径。
floyd的思想也可以用于判连通,比如杭电oj的判环问题,问题转换成有向图找环问题,先跑一个floyd,然后看邻接矩阵对角线上是否有1,也就是自身与自身连通即存在环,要记得对角线要初始化成0。如果想判断图是否存在负圈环检查是否存在dis[i][i]是负数即可。
思想基于贪心和动态规划,贪心在:每次循环扫一遍dis数组,其中最小值就是源点到此点的最短距离。这种贪心的正确性基于正权边,边是负权贪心将不正确。因为a>c,b>c ==> a+b>c当a,b,c不全为正值的时候不成立。比如边权-5+(-6)<(-10),具体这种模型如何构造就不说了。
Dijkstra实现贪心的过程要寻找dis数组最小值,所以可以用优先队列来优化。
也是牛的不行的算法,思维精炼强度大,也是基于动态规划的思想,代码实现也极其简单。
for(int k=1;k<=n-1;k++) for(int i=1;i<=m;i++) if(dist[u[i] ]!=inf&&dist[v[i] ] > dist[u[i] ]+w[i] ) dist[v[i]]=dist[u[i] ]+w[i];
bellman在前k次循环后的意义:(这里是难点)
条件:从源点到达某些点的最短路只需“最多经过k条边”,设这些点的点集为{A}。(要注意“只需”,“最多”,和“k条边”(不是floyd的点,也不是floyd的前k个))
前k次循环结束的意义现在可以便捷地表述为:在前k个循环结束后,目前已经确定了源点到这些点{A}的最短路。
所以一个图要是有n个点,那么就要循环n-1次才能保证目前的dist数组记录了到所有点的最短路,因为任何点到源点经历的边都不会超过n-1个。可能有人会像,既然循环前k结束后就已经确定了源点到{A}这些点的最短路,为什么不把这些点{A}剔除掉呢,下次不就不用在循环面执行if了么,反正if里面的值肯定是假。的确是这样,但是我们也不知道那些点满足{A}的性质,所以为了保证求出源点到某一个点的最短路,所以要循环n-1次。
显然这里有一个不等式的思想,循环n-1只是“保证”,显然可能在n-1次循环前的某一次循环结束后就已经确定了dist,进行下一轮循环后dist数组保持不变,这时就可以跳出循环。这是bellman的一个优化。另一个优化在后面说(其实就是SPFA)。
bellman算法的这个特性可以用来检测一个图是否含有负权回路,如果进行n-1轮松弛后,仍然存在:
for(int i=1;i<=m;i++) if(dist[u[i]]!=inf&&dist[v[i]]>dist[u[i]]+w[i]) flag=1;
是基于bellman的队列优化。这也算个算法我也无语了,模拟bellman的实现过程可以知道,每次循环后dist数组中的某些值可能不发生改变,这些不发生改变的值在下次循环中依然不发生改变,所以没必要在放在循环里面进行if判断了。所以每次循环后把dist数组中改变的值对应的顶点放在队列里面,下次循环只需对这些顶点进行出边松弛即可。需要注意的是:同一个顶点同时在队列里面不能出现多次,所以把点加入到队列前需要判重。代码如下:
#include<cstdio> #include<iostream> #include<queue> #include<cstring> #define inf 0x7fffffff using namespace std; int n,m; int u[404],v[404],w[404]; int first[22]; int next[404]; int dist[22]; bool book[22]; queue<int >que; void ini() { cin>>n>>m; memset(first,-1,sizeof(first)); for(int i=1;i<=m;i++){ cin>>u[i]>>v[i]>>w[i]; next[i]=first[u[i] ]; first[u[i] ]=i; } } void bellman_ford(int src) { for(int i=1;i<=n;i++) dist[i]=inf; dist[src]=0; que.push(src); book[src]=1; //要注意开始时要把源点入队。 while(!que.empty()){ //若一直循环则有负权回路,所以可以用cnt[v]记录每个顶点的入队次数,若大于n则有负权环 int cur=que.front(); que.pop(); book[cur]=0; //标记出队 int k=first[cur]; while(k!=-1){ if(dist[u[k] ]!=inf&& dist[v[k]]>dist[u[k] ]+w[k] ) { dist[v[k] ]=dist[u[k] ]+w[k]; if(!book[v[k]]) que.push(v[k]); book[v[k] ]=1; //标记入队 } k=next[k]; } } } int main() { ini(); bellman_ford(1); for(int i=1;i<=n;i++){ printf("%d ",dist[i]); } return 0; }
floyd和bellman的相似之处都是基于动态规划,floyd的着眼点是经过任何两点之间的最短路最多经过n-1个点,bellman的着眼点是任何两点的最短路最多经过n-1个边。
Dijktra和SPFA也很像,都是对点进行出边松弛一步一步确定dist数组,其实Dijktra就是在正权的时候贪心+bellman,但是不容易看出它和bellman到底哪里像。
在这里详述一下,因为基于bellman的算法具体步骤才出现了队列优化的SPFA,而SPFA在实现队列优化的过程中对每次循环后与源点距离变的点都被加入到队列中,然后下次循环随便取出一个点即可。所以bellman的优化不仅仅可以用队列来实现,也可以应stack,可以用任何容器,因为下次循环的只要对上次循环距离产生变化的点进行出边松弛即可,不管什么顺序。这里就发现另一个事实:每一个出队的点再下一次都可能再进队,有没有一种方式让出队的点进队次数少一点或者出队之后就保证不需进队了,在正权的时候很容易找到一种贪心法,如果每条边都是正值,每次循环前若队列里面存在一点到源点的距离最短,那么就可以断言源点到这点的最短距离已经确定,就是此时dist数组中的值,这种贪心证明就不述了。
所以,可以在正权的时候用优先队列(最小堆)优化SPFA,这样每次出队的点就不会再次进队,突然发现这中算法就是Dijkstra,所以我说Dijkstra是正权的时候贪心+bellman或说贪心+SPFA。但其实,在稀疏图中对单源问题来说SPFA的效率略高于 Heap+Dijkstra ,可以先说一下定性的认知,虽说dijstra是spfa+贪心,但是要基于heap实现,这种实现有失也有得,可以让每次点进队次数少,但排序损失了一部分时间,总和来说会略低于spfa,但稠密图的话dijstra的贪心优势就体现出来了,这种证明可以找专门的算法资料。
最后引用一下这位大神的博客结论(虽然目前不知为何,但希望以后能有用)
完。