算法学习:最短路径(Floyd、Bellman-ford、Dijkstra)

一、使用应用场景
(1)图的规模小,用Floyd。如果边的权值有负数,需要判断负圈。
(2)图的规模大,且边的权值非负,用Dijkstra。
(3)图的规模大,且边的权值有负数,用SPFA。需要判断负圈。
后面的讲解都已hdu 2544为例,讲解不同算法的思想以及模板代码。

Problem Description
在每年的校赛里,所有进入决赛的同学都会获得一件很漂亮的t-shirt。但是每当我们的工作人员把上百件的衣服从商店运回到赛场的时
候,却是非常累的!所以现在他们想要寻找最短的从商店到赛场的路线,你可以帮助他们吗?
Input
输入包括多组数据。每组数据第一行是两个整数N、M(N<=100,M<=10000),N表示成都的大街上有几个路口,标号为1的路口是商店所在
地,标号为N的路口是赛场所在地,M则表示在成都有几条路。N=M=0表示输入结束。接下来M行,每行包括3个整数A,B,C(1<=A,B<=N,1<=
C<=1000),表示在路口A与路口B之间有一条路,我们的工作人员需要C分钟的时间走过这条路。
输入保证至少存在1条商店到赛场的路线。
Output
对于每组输入,输出一行,表示工作人员从商店走到赛场的最短时间
Sample Input
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
Sample Output
3
2

二、Floyd
1.所有点对间的最短路径
Floyd用到了动态规划的思想:求两点i、j之间的最短距离,可以分为两种情况考虑,即经过图中某个点k的路径和不经过点k的路径,取两者中的最短路径。
动态规划的过程可以描述为:
(1)令k=1,计算所有结点之间(经过结点1、不经过结点1)的最短路径。
(2)令k=2,计算所有结点之间(经过结点2、不经过结点2)的最短路径,这次计算利用了k=1时的计算结果。
(3)令k=3…
可以想象这样一个过程:
(1)图中有n个结点,m条边。
(2)把图上的每个点看成一个灯,初始时灯都是灭的,大部分结点之间的距离被初始化为无穷大INF,除了m条边连接的那些结点以外。
(3)从结点k=1开始操作,想象点亮了这个灯,并以k=1位中转点,计算和调整图上所有点之间的最短距离。很显然,对这个灯的邻居进行的计算是有效的,而对远离它的那些点的计算基本是无效的。
(4)逐步点亮所有的灯,每次点灯,就用这个灯中转,重新计算和调整所有灯之间的最短距离,这些计算用到了以前点灯时得到的计算结果。
(5)灯逐渐点亮,知道图上的点全亮,计算结束。
在这个过程中,由于很多计算都是无效的,所以算法的效率并不高。
floyd()有3重循环,复杂度是O(n^3),只能用于计算规模很小的图,即n<200的情况。

Floyd算法代码如下:

#include
using namespace std;
const int INF=1e6;
const int NUM=105;
int graph[NUM][NUM];//用邻接矩阵存图
int n,m;
void floyd(){
    int s=1;
    for(int k=1;k<=n;k++)//定义起点
        for(int i=1;i<=n;i++)
            if(graph[i][k]!=INF)//一个小优化
                for(int j=1;j<=n;j++)
                    if(graph[i][j]>graph[i][k]+graph[k][j])
                        graph[i][j]=graph[i][k]+graph[k][j];
                    //也可以直接用min函数,但是效率会比较低
    printf("%d\n",graph[s][n]);
}
int main(){
    while(~scanf("%d%d",&n,&m)){
        if(!n&&!m)return 0;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)graph[i][j]=INF;//任意两点间距离无穷
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            graph[a][b]=graph[b][a]=c;
        }
        floyd();
    }
}

Floyd算法虽然低效,但是也有优点:
(1)程序很简单;
(2)可以一次求出所有结点之间的最短路径;
(3)能处理有负权边的图。

2、判断负圈
程序里有个有趣的地方。在程序中,结点i到自己的距离graph[i][i]并没有置初值为0,而是INF。并且在计算结束之后,graph[i][i]也不是0,而是graph[i][i]=graph[i][u]+…+graph[v][i],即到外面绕一圈回来的最小路径。这一点可用于判断负圈。

负圈是这个产生的:如果某些边的权值是负数,那么图中可能存在有这样的环路,环路上边的权值之和为负数,这样的环路就是负圈。每走一次这个负圈,总权值就会更小,导致陷在这个圈里出不来。

利用Floyd算法很容易判断负圈,只要在floyd()中判断是否存在某个graph[i][i]<0即可。此时可置graph[i][i]的值为0,加快判断过程。

3、Bellman-Ford
1、Bellman-Ford算法
Bellman-Ford算法用来解决单源最短路径问题:给定一个起点s,求它到图中所有n个结点的最短路径。
Bellman-Ford算法的特点是只对相邻结点进行计算,可以避免Floyd那种大撒网式的无效计算,大大提高效率。可以想象图上的每个点都有一个人,初始时,所有人到s的距离都为INF,即无限大。用下列步骤求最短路径:
(1)第一轮,给所有的n个人一个机会,问他的邻居到s的最短距离是多少?如果他的邻居到s的距离不是INF,他就能借道这个邻居到s去,并且把自己原来的INF更新为较短的距离。显然,开始的时候,起点s的直连邻居(例如u)肯定能更新距离,而u的邻居(例如v),如果在u更新之后问v,那么v有机会更新,否则就只能保持INF不变。特别地,在第一轮更新中,存在一个与s最近的邻居t,t到s的距离就是全图中t到s的最短距离。因为它通过别的邻居绕路到s,肯定更远。t的最短距离已经得到,后面不会再更新,称一轮更新为“松弛”。
(2)第二轮,重复第一轮的操作,再给每个人一次问邻居的机会。这一轮操作之后,至少存在一个s或t的邻居v,可以算出它到s的最短距离。v要么和s直连,要么是通过t到达s的。v的最短距离也得到了,后面不会再更新。
(3)第三轮,再给,每个人一个机会…
继续以上操作,直到所有人都不能再更新最短距离为止。复杂度为O(mn)。

以上过程,每个结点可以独立进行计算,所以这个算法符合并行计算的思想,可以用在并行计算上。例如计算机网络的BGP路由协议,每个路由器是一个结点,它根据与邻居的信息交换,独自计算到网络中其它路由器的最短距离。

Bellman-Ford的每一轮操作只需要检查存在的m条边。在n*n的邻接矩阵中,这m条边是那些不等于INF的边,但是要使用Floyd算法那样的邻接矩阵,则不得不检查所有的边。

下面的程序对存储进行了优化,用struct edge e[10005]数组来存m条边,避免了存储那些不存在的边。这种简单的存储方法不是邻接表,不能快速搜一个结点的所有邻居,不过正适合Bellman-Ford这种简单的算法。

代码如下:

#include
using namespace std;
const int INF=1e6;
const int NUM=105;
struct edge{int u,v,w;}e[10005];
int n,m,cnt;
int pre[NUM];//记录前驱结点。pre[x]=y,在最短路径上,x的前一个结点是y
void print_path(int s,int t){
    if(s==t){printf("%d",s);return;}
    print_path(s,pre[t]);
    printf("%d",t);
}
void bellman(){
    int s=1;
    int d[NUM];//d[i]记录第i个结点到起点s的最短距离
    for(int i=1;i<=n;i++)d[i]=INF;
    d[s]=0;
    for(int k=1;k<=n;k++)
        for(int i=0;i<cnt;i++){
            int x=e[i].u,y=e[i].v;
            if(d[x]>d[y]+e[i].w)
                d[x]=d[y]+e[i].w,pre[x]=y;
        }
    printf("%d\n",d[n]);
    //print_path(s,n);
}
int main(){
    while(~scanf("%d%d",&n,&m)){
        if(!n&&!m)return 0;
        cnt=0;
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            e[cnt].u=a,e[cnt].v=b,e[cnt].w=c,cnt++;
            e[cnt].u=b,e[cnt].v=a,e[cnt].w=c,cnt++;
        }
        bellman();
    }
}

3、Dijkstra
也可以用来解决单源最短路径问题。是非常高效和稳定的算法,但是会更复杂,思想如下:
在图中所含有的边上排满多米诺骨牌,相当于把骨牌看成图的边。一条边上的多米诺骨牌数量和边的权值(长度或费用)成正比。规定所有骨牌倒下的速度都是一样的。如果在一个结点上推到骨牌,会导致这个结点上的所有骨牌都往后面倒下去。
在起点s推倒骨牌,可以观察到,从s开始,它连接的边上的骨牌都逐渐倒下,并到达所有能达到的结点。在某个结点t,可能先后从不同的线路倒骨牌下来;先倒过来的骨牌,其经过的路径一定就是从s到t的最短路径;后倒过来的骨牌,对确定最短路径没有贡献,不用管。
从整体上看,这就是一个从起点s扩散到整个图的过程。
在这个过程中,观察所有结点的最短路径是这样得到的:
(1)在s的所有直连邻居中,最近的邻居u,骨牌首先到达。u是第一个确定最短路径的结点。从u直连到s的路径肯定是最短的,因为如果u绕道别的结点到s,必然更远。
(2)然后,把后面骨牌的倒下分为两部分,一部分是从s继续倒下到s的其他的直连邻居,另一部分是从u出发倒下到u的直连邻居。那么下一个到达的结点v必然是s或者u的一个直连邻居。v是第二个确定最短路径的结点。
(3)继续以上步骤。。。。。。
Dijkstra算法应用了贪心法的思想,即“抄近路走,肯定能找到最短路径”。
从以上步骤可以发现:Dijkstra的每次迭代,只需要检查上次已经确定最短路径的那些结点的邻居,检查范围很小,算法是高效的;每次迭代,都能得到至少一个结点的最短路径,算法是稳定的。
如何实现编程?程序的主要内容是维护两个集合,即已确定最短路径的结点集合A、这些结点向外扩散的邻居结点B。程序逻辑如下:
(1)把起点s放到A中,把s所有的邻居放到B中,此时,邻居到s的距离就是直连距离。
(2)从B中找到距离起点s最短的结点u,放到A中。
(3)把u所有的新邻居放到B中。显然,u的每一条边都连接了一个邻居,每个新邻居需要加进去。其中u的一个新邻居v,它到s的距离dis(s,v)等于dis(s,u)+dis(u,v)。
(4)重复2、3,直到B为空时结束。
计算结束后,可以得到从起点s到其它所有点的最短距离。

方法可以改进,得到更好的复杂度,改进方法如下:
(1)每次往B中放新数据时按从小到大的顺序放,用二分法的思路,复杂度是O(logn),保证最小的数总在前面。
(2)找最小值,直接取B的第一个数,复杂度为O(1)。
此时Dijkstra算法的总复杂度是O(mlogn),是最高效的最短路径算法。
编程时,一般不用自己写上面的程序,直接用STL的优先队列就行了,完成数据的插入和提取。
下面的程序代码有两个关键技术:
(1)用邻接表存图和查找邻居。对邻居的查找和扩展是通过动态数组vector< edge >e[NUM]实现的邻接表。其中e[i]存储第i个结点上所有的边,边的一头是它的邻居,即struct edge的参数to。在需要扩展结点i的邻居的时候,查找e[i]即可。已经放到集合A中的结点不要扩展,程序中用bool done[NUM]记录集合A,当done[i]=true时,表示它在集合A中,已经找到了最短路径。
(2)在集合B中找距离起点最短的结点。直接用STL的优先队列实现,在程序中是priority_queue< s_node >Q。但是有关丢弃的动作,STL的优先队列无法做到。在程序中也是用bool done[NUM]协助解决这个问题。

代码如下:

#include
using namespace std;
const int INF=1e6;
const int NUM=105;
struct edge{
    int from,to,w;
    edge(int a,int b,int c){from=a;to=b;w=c;}
};
vector<edge>e[NUM];
struct s_node{
    int id,n_dis;//定义结点和这个结点到起点的距离
    s_node(int b,int c){id=b;n_dis=c;}
    bool operator<(const s_node &a)const
    {return n_dis>a.n_dis;}//在结构体做好排序
};
int n,m;
int pre[NUM];//记录前驱结点
void print_path(int s,int t){
    ;
}
void dijkstra(){
    int s=1;//起点s
    int dis[NUM];//记录所有结点到起点的距离
    bool done[NUM];//done[i]为true时表示结点i的最短路径已经找到
    for(int i=1;i<=n;++i)dis[i]=INF,done[i]=false;//初始化
    dis[s]=0;//起点到自己的距离为0
    priority_queue<s_node>Q;//优先队列,存结点信息
    Q.push(s_node(s,dis[s]));//起点进队列
    while(!Q.empty()){
        s_node u=Q.top();//pop出距起点s距离最小的结点u
        Q.pop();
        if(done[u.id])continue;
        //丢弃已经找到最短路径的结点,即集合A中的结点
        done[u.id]=true;
        for(int i=0;i<e[u.id].size();++i){//检查结点u的所有邻居
            edge y=e[u.id][i];//u.id的第i个邻居是y.to
            if(done[y.to])continue;//丢弃已经找到最短路径的邻居结点
            if(dis[y.to]>y.w+u.n_dis){
                dis[y.to]=y.w+u.n_dis;
                Q.push(s_node(y.to,dis[y.to]));
                //拓展新的邻居,放入优先队列中
                pre[y.to]=u.id;
            }
        }
    }
    printf("%d\n",dis[n]);
}
int main(){
    while(~scanf("%d%d",&n,&m)){
        if(!n&&!m)return 0;
        for(int i=1;i<=n;i++)e[i].clear();
        while(m--){
            int a,b,c;
            scanf("%d%d%d",&a,&b,&c);
            e[a].push_back(edge(a,b,c));
            e[b].push_back(edge(b,a,c));
        }
        dijkstra();
    }
}

你可能感兴趣的:(算法学习,算法,图论,acm竞赛,数据结构)