zoukankan      html  css  js  c++  java
  • 《啊哈算法》——最短路径

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

      所谓最短路径问题,就是基于一个图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]);
    
    }
  • 相关阅读:
    Netty指定分隔的字符
    Netty解决TCP粘包/拆包问题
    TCP粘包/拆包问题
    Netty5-应答服务器
    线程池中的线程的排序问题
    Java NIO 概述
    在windows中,如何使用cmd命令行窗口正确显示编码为utf-8格式的文字
    OXM
    Mysql event时间触发器,实现定时修改某些符合某一条件的某一字段
    linux 环境 tomcat 莫名奇妙挂掉
  • 原文地址:https://www.cnblogs.com/rhythmic/p/5452083.html
Copyright © 2011-2022 走看看