《啊哈算法》——最短路径

  虽然笔者在“算法-图论”的专栏中已经讨论过有关最短路径的问题,但是这里还是重新讨论一下,孔子也说过嘛,温故而知新。

  所谓最短路径问题,就是基于一个图G<V、E>,图的边集E是带权的,然后讨论寻求某条连通两个点的路径,使得这条路径是所有连通该路径中边权最小的。

  找到任意两点间的最短路径——Floyd-Warshall算法。

  这个算法是个本科念文学后来转计算机并于1978年荣获图灵奖的Robert W.Floyd和Stephen Warshall于1962同时独立开发出来的,因此为了纪念他们,该算法融合了两个人的名字。

  其实这个算法说来思路也非常简单,我们当前想要求得G中vi到vj的最短路径,最简单暴力的方法是找到vi到vj所有可走的路径,然后维护一个最小值即可。然而现在问题是如何找到这所有的路径?我们找一个中间变量k,vi->vj的路径可以分解成vi->vk 、vk->vj的路径,而将k遍历[i,j]便可枚举出所有情况,你可能会问那vi->vk还有很多路径呢,那岂不还要继续找?事实上,我们设置e[i][j]记录vi->vj的最短路径,而vi->vk、vk->vj则也是基于e[i][k]、e[k][j]的,这样我们其实是枚举了一系列相对最优的情况,然后找到这些相对最优解中的当前最优解的情况,这便是动态规划思想所体现的地方。

  本质上讲,该算法是枚举和动态规划的一个巧妙的结合。

#include<cstdio>
using namespace std;

int main()
{
     int e[10][10] , k , i , j , n , m , t1,t2,t3;
     int inf = 9999999;
     scanf("%d%d",&n,&m);

     for(i = 1;i<=n;i++)
          for(j = 1;j<=n;j++)
             if(i == j)  e[i][j] = 0;
             else        e[i][j] = inf;

    for(i = 1;i<=m;i++)
    {
        scanf("%d%d%d",&t1,&t2,&t3);
        e[t1][t2] = t3;
    }

       for(k=1;k<=n;k++)
          for(i=1;i<=n;i++)
             for(j=1;j<=n;j++)
                if(e[i][j] > e[i][k] + e[k][j]) //Floyd-Warshall算法的核心部分
                   e[i][j] = e[i][k] + e[k][j];


       for(i=1;i<=n;i++)
       {
              for(j = 1;j<=n;j++)
                  printf("%10d",e[i][j]);

              printf("
");
       }
       return 0;

}

  求解单源无负权的最短路径——Dijkstra算法:

  我们从该算法的限制条件出发,既然是无负权,我们能够发现,如果我们筛选与源头s直接相连的点,便可以直接找到源头s到某点的最短路径(因为无负权边,则不可能出现中转点),我们基于选出来的这个点,假设其为vi,访问与vi直接相连的点vj、vk、vm……,则我们当前可以得到源头s到vj、vk、vm的一条路径,此时我们需要记录下路径<s,vj>、<s、vk>、<s、vm>的权重。然后我们继续遍历与源头s直接相连的点(显然第一次筛选出来的点需要标记访问过,在这次筛选中边不再访问),然后重复上述步骤……这样的操作进行n - 1次,将确保得到源头到每个点的每条路径,我们只需在访问时维护最小值即可。

  以上是对该算法比较模糊和概括性的描述,不难看出,其算法本质是一种巧妙的穷举,然后在穷举的过程中完成对最短路径的记录。

  下面我们来描述一下该算法比较量化、精细的描述。

  step1:将所有的顶点分为两部分:访问过的顶点放入Q,未曾访问过的顶点放入P。一开始源头s在Q中。

  step2:用dis[i]表示第i个顶点到源头的最短路径,访问点集V,满足vi∈P 并且vi与源头s直接相连,找到min<s,vi>,标记vi访问过并记录i和dis[i]。

  step3:访问点集V1,对于vj∈V1,满足step2中记录的vi与vj直接相连,此时可访问一条路径<s,vj>,记录dis[j]。

  step4:进行step2,直到P是空集。

  简单的参考代码如下。

#include<stdio.h>
int main()
{
     int e[10][10] , dis[10]  , book[10] , i , j , n, m , t1,t2,t3,u,v,Min;
     int inf = 9999999;
     scanf("%d %d",&n,&m);

     for(i = 1;i <= n;i++)
          for(j = 1;j <= n;j++)
              if(i == j)  e[i][j] = 0;
              else        e[i][j] = inf;

              for(i = 1;i <= m;i++)
              {
                    scanf("%d %d %d",&t1,&t2,&t3);
                    e[t1][t2] = t3;
              }

               for(i = 1;i <= n;i++)
                    dis[i] = e[1][i];

               for(i = 1;i<=n;i++)
                   book[i] = 0;
               book[1] = 1;

          for(i = 1;i <= n-1;i++)  //筛出当前没有访问的并且与源头直接相连的全职最小的点。
        {
               Min = inf;
               for(j = 1;j <= n;j++)
               {
                     if(book[j] == 0&& dis[j] < Min)
                     {
                           Min = dis[j];
                           u = j;
                     }
               }


          book[u] = 1;
          for(v = 1;v <= n;v++) //边松弛
          {
                if(e[u][v] < inf)
                {
                      if(dis[v] > dis[u] + e[u][v])
                           dis[v] = dis[u] + e[u][v];
                }
          }
        }

        for(i  = 1;i <= n;i++)
               printf("%d ",dis[i]);

    return 0;
}

  解决含负权图的Bellman-Ford算法:

  其实从最本质上来讲,最短路径都是某种程度的穷举和动态规划的结合,只是在不同的算法中呈现出不同的方式。

  给出一个含负权的有向图,我们如何求解各点到某一点(即单源最短路)的最短路径呢?Bellman-Ford给出了一个非常简洁的方法。

  我们利用dis[i]表示vi到源头v1的最短路,用s[i]、e[i]分别记录第i条边的起点和终点,用w[i]记录该边的权值。首先枚举边<s[i] , e[i]>,我们访问dis[e[i]],并依据这条边的权值来实现边权值,即用一个状态转移方程来表述——dis[e[i]] = min(dis[e[i]] ,dis[s[i]] + w[i])。能够看到,这种状态转移方程要基于dis[s[i]]的确定,而每次枚举边集合,至少能够完成一个点访问和记录,因此对于含有n个节点的图,枚举边集n-1次便可找到源头v1到剩余n-1个点的最短路径。

  我们用伪码来描述一下该过程。

  fo k 1 to n-1 (n为节点数)

     for i 1 to m  (m为边数)

          dis[e[i]] = min(dis[e[i]] ,dis[s[i]] + w[i]).

  简单的参考代码如下。

#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int main()
{
     int dis[10] , i , k , n , m , s[10] , e[10] , w[10];
     int inf = 9999999;
     scanf("%d %d",&n,&m);

     for(i  = 1;i <= m;i++)
           scanf("%d %d %d",&s[i],&e[i],&w[i]);

     for(i = 1;i <= n;i++)
          dis[i] = inf;
     dis[1] = 0;

     for(k = 1;k <= n;k++)
         for(i = 1;i <= m;i++)
               dis[e[i]] = min(dis[e[i]] , dis[s[i]] + w[i]);

      for(i = 1;i <= n;i++)
           printf("%d ",dis[i]);

}
原文地址:https://www.cnblogs.com/rhythmic/p/5452083.html