Thoroughly understand the shortest path problem (reproduced)

Original link: http://www.cnblogs.com/hxsyl/p/3270401.html

(really great, than heart)

 I just want to say: review the old and learn the new, you can be a teacher. My sophomore's "Data Structure" was taught by Mr. Shen. I didn't understand it very well at that time. It was probably too theoretical (ps: maybe it was because I slept); I read Lao Wang's 2011 courseware again today, I told it again to the sophomore children, and I googled a lot of information, and I completely understood the shortest path problem. Readers please enjoy...

        I firmly believe that there are no bad students, only rubbish education. But no one treats you for granted, so learn to be grateful.

1. Problem introduction

        Problem: Starting from a vertex, among the paths taken along the edge of the graph to another vertex, the path with the smallest sum of weights on each edge is the shortest path. There are the following algorithms to solve the shortest path problem, Dijkstra algorithm, Bellman-Ford algorithm, Floyd algorithm and SPFA algorithm, as well as the famous heuristic search algorithm A* , but A* is going to be a separate article, in which the Floyd algorithm can solve any arbitrary The length of the shortest path between two points. The author believes that any shortest path algorithm is based on the fact that there are only two possibilities for the shortest path from any node A to any node B, 1 is directly from A to B, 2 is from A through several nodes to B .

2. Dijkstra algorithm

        The algorithm is explained in a greedy form in the textbook "Data Structure", but it is arranged in the dynamic programming chapter in the textbook "Operations Research", and readers are advised to read both.

           image

        Observe the table on the right and find that all but the last node have found the shortest path.

        (1) Dijkstra's algorithm generates the shortest path in increasing order of path length (see the last row of the table below, which is the next point). First divide V into two groups:

  • S: The set of vertices for which the shortest path has been found
  • VS=T: The set of vertices for which the shortest path has not yet been determined

        将T中顶点按最短路径递增的次序加入到S中,依据:可以证明V0到T中顶点Vk的最短路径,或是从V0到Vk的直接路径的权值或是从V0经S中顶点到Vk的路径权值之和(反证法可证,说实话,真不明白哦)。

        (2)   求最短路径步骤

  1. 初使时令 S={V0},T={其余顶点},T中顶点对应的距离值, 若存在<V0,Vi>,为<V0,Vi>弧上的权值(和SPFA初始化方式不同),若不存在<V0,Vi>,为Inf。
  2. 从T中选取一个其距离值为最小的顶点W(贪心体现在此处),加入S(注意不是直接从S集合中选取,理解这个对于理解vis数组的作用至关重要),对T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值比不加W的路径要短,则修改此距离值(上面两个并列for循环,使用最小点更新)。
  3. 重复上述步骤,直到S中包含所有顶点,即S=V为止(说明最外层是除起点外的遍历)。

        下面是上图的求解过程,按列来看,第一列是初始化过程,最后一行是每次求得的next点。

           image

        (3)   问题:Dijkstar能否处理负权边?(来自《图论》)

             答案是不能,这与贪心选择性质有关(ps:貌似还是动态规划啊,晕了),每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径;但如果存在负权边,那就有可能先通过并不是距源点最近的一个次优点(dmin'),再通过这个负权边L(L<0),使得路径之和更小(dmin'+L<dmin),则dmin'+L成为最短路径,并不是dmin,这样dijkstra就被囧掉了。比如n=3,邻接矩阵: 
0,3,4 
3,0,-2 
4,-2,0,用dijkstra求得d[1,2]=3,事实上d[1,2]=2,就是通过了1-3-2使得路径减小。不知道讲得清楚不清楚。

二.Floyd算法

        参考了南阳理工牛帅(目前在新浪)的博客。

        Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离。

        很简单吧,代码看起来可能像下面这样:

for (int i=0; i<n; ++i) {
  for (int j=0; j<n; ++j) {
    for (int k=0; k<n; ++k) {
      if (dist[i][k] + dist[k][j] < dist[i][j] ) {
        dist[i][j] = dist[i][k] + dist[k][j];
      }
    }
  }
}

        但是这里我们要注意循环的嵌套顺序,如果把检查所有节点K放在最内层,那么结果将是不正确的,为什么呢?因为这样便过早的把i到j的最短路径确定下来了,而当后面存在更短的路径时,已经不再会更新了。

        让我们来看一个例子,看下图:

image

        图中红色的数字代表边的权重。如果我们在最内层检查所有节点K,那么对于A->B,我们只能发现一条路径,就是A->B,路径距离为9,而这显然是不正确的,真实的最短路径是A->D->C->B,路径距离为6。造成错误的原因就是我们把检查所有节点K放在最内层,造成过早的把A到B的最短路径确定下来了,当确定A->B的最短路径时dist(AC)尚未被计算。所以,我们需要改写循环顺序,如下:

        ps:个人觉得,这和银行家算法判断安全状态(每种资源去测试所有线程),树状数组更新(更新所有相关项)一样的思想。

for (int k=0; k<n; ++k) {
  for (int i=0; i<n; ++i) {
    for (int j=0; j<n; ++j) {
            /*
            实际中为防止溢出,往往需要选判断 dist[i][k]和dist[k][j
            都不是Inf ,只要一个是Inf,那么就肯定不必更新。 
            */
      if (dist[i][k] + dist[k][j] < dist[i][j] ) {
        dist[i][j] = dist[i][k] + dist[k][j];
      }
    }
  }
}

        如果还是看不懂,那就用草稿纸模拟一遍,之后你就会豁然开朗。半个小时足矣(早知道的话会节省很多个半小时了。。cunning

       再来看路径保存问题:

void floyd() {
      for(int i=1; i<=n ; i++){
        for(int j=1; j<= n; j++){
          if(map[i][j]==Inf){
               path[i][j] = -1;//表示  i -> j 不通 
          }else{
               path[i][j] = i;// 表示 i -> j 前驱为 i
          }
        }
      }
      for(int k=1; k<=n; k++) {
        for(int i=1; i<=n; i++) {
          for(int j=1; j<=n; j++) {
            if(!(dist[i][k]==Inf||dist[k][j]==Inf)&&dist[i][j] > dist[i][k] + dist[k][j]) {
              dist[i][j] = dist[i][k] + dist[k][j];
              //path[i][k] = i;//删掉
              path[i][j] = path[k][j];
            }
          }
        }
      }
    }
    void printPath(int from, int to) {
        /*
         * 这是倒序输出,若想正序可放入栈中,然后输出。
         * 
         * 这样的输出为什么正确呢?个人认为用到了最优子结构性质,
         * 即最短路径的子路径仍然是最短路径
         */
        while(path[from][to]!=from) {
            System.out.print(path[from][to] +"");
            to = path[from][to];
        }
    }

        《数据结构》课本上的那种方式我现在还是不想看,看着不舒服……

        Floyd算法另一种理解DP,为理论爱好者准备的,上面这个形式的算法其实是Floyd算法的精简版,而真正的Floyd算法是一种基于DP(Dynamic Programming)的最短路径算法。设图G中n 个顶点的编号为1到n。令c [i, j, k]表示从i 到j 的最短路径的长度,其中k 表示该路径中的最大顶点,也就是说c[i,j,k]这条最短路径所通过的中间顶点最大不超过k。因此,如果G中包含边<i, j>,则c[i, j, 0] =边<i, j> 的长度;若i= j ,则c[i,j,0]=0;如果G中不包含边<i, j>,则c (i, j, 0)= +∞。c[i, j, n] 则是从i 到j 的最短路径的长度。对于任意的k>0,通过分析可以得到:中间顶点不超过k 的i 到j 的最短路径有两种可能:该路径含或不含中间顶点k。若不含,则该路径长度应为c[i, j, k-1],否则长度为 c[i, k, k-1] +c [k, j, k-1]。c[i, j, k]可取两者中的最小值。状态转移方程:c[i, j, k]=min{c[i, j, k-1], c [i, k, k-1]+c [k, j, k-1]},k>0。这样,问题便具有了最优子结构性质,可以用动态规划方法来求解。

        看另一个DP(直接引用王老师课件)

                       image

 

        说了这么多,相信读者已经跃跃欲试了,咱们看一道例题,以ZOJ 1092为例:给你一组国家和国家间的部分货币汇率兑换表,问你是否存在一种方式,从一种货币出发,经过一系列的货币兑换,最后返回该货币时大于出发时的数值(ps:这就是所谓的投机倒把吧),下面是一组输入。 
3    //国家数 
USDollar  //国家名 
BritishPound 
FrenchFranc 
   3    //货币兑换数 
USDollar 0.5 BritishPound  //部分货币汇率兑换表 
BritishPound 10.0 FrenchFranc 
FrenchFranc 0.21 USDollar

        月赛做的题,不过当时用的思路是求强连通分量(ps:明明说的,那时我和华杰感觉好有道理),也没做出来,现在知道了直接floyd算法就ok了。

        思路分析:输入的时候可以采用Map<String,Integer> map = new HashMap<String,Integer>()主要是为了获得再次包含汇率输入时候的下标以建图(感觉自己写的好拗口),或者第一次直接存入String数组str,再次输入的时候每次遍历str数组,若是equals那么就把str的下标赋值给该币种建图。下面就是floyd算法啦,初始化其它点为-1,对角线为1,采用乘法更新求最大值。

三.Bellman-Ford算法

        为了能够求解边上带有负值的单源最短路径问题,Bellman(贝尔曼,动态规划提出者)和Ford(福特)提出了从源点逐次绕过其他顶点,以缩短到达终点的最短路径长度的方法。Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛,每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。

        关于SPFA,请看我这一篇http://www.cnblogs.com/hxsyl/p/3248391.html

        递推公式(求顶点u到源点v的最短路径):

         dist 1 [u] = Edge[v][u]

         dist k [u] = min{ dist k-1 [u], min{ dist k-1 [j] + Edge[j][u] } }, j=0,1,…,n-1,j≠u

         Dijkstra算法和Bellman算法思想有很大的区别:Dijkstra算法在求解过程中,源点到集合S内各顶点的最短路径一旦求出,则之后不变了,修改  的仅仅是源点到T集合中各顶点的最短路径长度。Bellman算法在求解过程中,每次循环都要修改所有顶点的dist[ ],也就是说源点到各顶点最短路径长度一直要到Bellman算法结束才确定下来。

        算法适用条件

  • 1.单源最短路径(从源点s到其它所有顶点v)
  • 有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图)
  • 边权可正可负(如有负权回路输出错误提示)
  • 差分约束系统(至今貌似只看过一道题)

        Bellman-Ford算法描述:

  1. 初始化:将除源点外的所有顶点的最短距离估计值 d[v] ←+∞, d[s] ←0
  2. 迭代求解:反复对边集E中的每条边进行松弛操作,使得顶点集V中的每个顶点v的最短距离估计值逐步逼近其最短距离;(运行|v|-1次,看下面的描述性证明(当做树))
  3. 检验负权回路:判断边集E中的每一条边的两个端点是否收敛。如果存在未收敛的顶点,则算法返回false,表明问题无解;否则算法返回true,并且从源点可达的顶点v的最短距离保存在d[v]中

        描述性证明:(这个解释很好)

        首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。

其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按顶点距离s的层次,逐层生成这棵最短路径树的过程。

在对每条边进行1遍松弛的时候,生成了从s出发,层次至多为1的那些树枝。也就是说,找到了与s至多有1条边相联的那些顶点的最短路径;对每条边进行第2遍松弛的时候,生成了第2层次的树枝,就是说找到了经过2条边相连的那些顶点的最短路径……。因为最短路径最多只包含|v|-1条边,所以,只需要循环|v|-1 次。

每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,这就是Bellman-Ford算法效率底下的原因,也正是SPFA优化的所在)。

image,如图(没找到画图工具的射线),若是B和C的最短路径不更新,那么点D的最短路径肯定也无法更新,这就是优化所在。

如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。

如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。

           参考了《图论》。

        问题:Bellman-Ford算法是否一定要循环n-1次么?未必!其实只要在某次循环过程中,考虑每条边后,都没能改变当前源点到所有顶点的最短路径长度,那么Bellman-Ford算法就可以提前结束了(开篇提出的小优化就是这个)。

        上代码(参考了牛帅的博客)

#include<iostream>
#include<cstdio>
using namespace std;
 
   
#define MAX 0x3f3f3f3f
#define N 1010
int nodenum, edgenum, original; //点,边,起点
 
   
typedef struct Edge //边
{
  int u, v;
  int cost;
}Edge;
 
   
Edge edge[N];
int dis[N], pre[N];
 
   
bool Bellman_Ford()
{
  for(int i = 1; i <= nodenum; ++i) //初始化
    dis[i] = (i == original ? 0 : MAX);
  for(int i = 1; i <= nodenum - 1; ++i)
    for(int j = 1; j <= edgenum; ++j)
      if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(顺序一定不能反~)
      {
        dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
        pre[edge[j].v] = edge[j].u;
      }
      bool flag = 1; //判断是否含有负权回路
      for(int i = 1; i <= edgenum; ++i)
        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
        {
          flag = 0;
          break;
        }
        return flag;
}
 
   
void print_path(int root) //打印最短路的路径(反向)
{
  while(root != pre[root]) //前驱
  {
    printf("%d-->", root);
    root = pre[root];
  }
  if(root == pre[root])
    printf("%d\n", root);
}
 
   
int main()
{
  scanf("%d%d%d", &nodenum, &edgenum, &original);
  pre[original] = original;
  for(int i = 1; i <= edgenum; ++i)
  {
    scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
  }
  if(Bellman_Ford())
    for(int i = 1; i <= nodenum; ++i) //每个点最短路
    {
      printf("%d\n", dis[i]);
      printf("Path:");
      print_path(i);
    }
  else
    printf("have negative circle\n");
  return 0;
}

四.SPFA算法

        用一个队列来进行维护。初始时将源加入队列。每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束;这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法(看我上面那个图,只有相邻点更新了,该点才有可能更新) 。

         代码参见  http://www.cnblogs.com/hxsyl/p/3248391.html

五.趣闻

        整理该篇博文的时候,一哥们发布网站到我们群,网站很精美,一牛神(acmol)使用fork炸弹,结果服务器立马挂啦,更改后又挂啦,目测目前无限挂中。。。

六、欢迎加群

  This group was created on 2011/7/4: CodeForFuture (163354117, there is a link to the group on the right)... This group focuses on the Internet, e-commerce and data mining. The members of the group are graduate students and undergraduates from major universities. Students (such as Tsinghua University, Peking University, Sun Yat-Sen University, Beijing Union University, South China University of Technology, Jiangnan University, Beihang University, Institute of Computing Technology, Shanghai University, Beijing Post, Beijiao University, Shandong University, Wuhan Geology, Pingdingshan University, Tongji, Nanjing University of Technology, University of Sydney , Wuhan University of Technology, China University of Mining and Technology, Xi'an Science and Technology, China Normal University, Shandong Finance and Economics, Huake, Shangda, Hohai, Jida, Network Research Institute, Harbin Institute of Technology, Shandong Science and Technology, Xiamen University, Central South, Hunan University of Technology, Shenzhen University, Sichuan University, Harbin Business school...not to list them all) and employees of major companies (such as Baidu, Sina, Kingsoft, Aerospace Science and Technology Group, iQiyi, Huawei Technology, CCB, Ledou Games, etc.), and Headhunter... Looking forward to your joining, let's go from good to great together...

Author: Hoshi Ichiro
The copyright of this article belongs to the author, Mars Eleven. Welcome to reprint and commercial use, but this statement must be retained without the author's consent, and a link to the original text is given in an obvious position on the article page, otherwise the right to pursue legal responsibility is reserved.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324610800&siteId=291194637