C++算法篇 动态规划----最短路

 

对于无权的图来说:

若从一顶点到另一顶点存在着一条路径,则称该路径长度为该路径上所经过的边的数目,也等于该路径上的顶点数减1。

由于从一顶点到另一顶点可能存在着多条路径,每条路径上所经过的边数可能不同,即路径长度不同,我们把路径长度最短(即经过的边数最少)的那条路径叫做最短路径,其路径长度叫做最短路径长度或最短距离。

 

对于带权的图来说:

考虑路径上各边上的权值,则通常把一条路径上所经边的权值之和定义为该路径的路径长度或称带权路径长度。

 从源点到终点可能不止一条路径,把带权路径长度最短的那条路径称为最短路径,其路径长度(权值之和)称为最短路径长度或者最短距离。

 

 

一、弗洛伊德算法(Floyd)最短路算法

       Floyd算法又称为弗洛伊德算法、插点法,是解决给定的加权图中顶点间的最短路径的一种算法,可以正确处理有向图或负权的最短路径问题,同时也被用于计算有向图的传递闭包。该算法名称以创始人之一、1978年图灵奖获得者、斯坦福大学计算机科学系教授罗伯特·弗洛伊德命名。Robert W.Floyd这个牛人是朵奇葩,他原本在芝加哥大学读的文学,但是因为当时美国经济不太景气,找工作比较困难,无奈之下到西屋电气公司当了一名计算机操作员,在IBM650机房值夜班,并由此开始了他的计算机生涯。

适用范围:无负权回路即可,边权可正可负,运行一次算法即可求得任意两点间最短路。

优缺点:

Floyd是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法

优点:容易理解,可以算出任意两个节点之间的最短距离,代码编写简单

缺点:时间复杂度比较高,不适合计算大量数据。能解决负边(负权)但不能解决负环。

时间复杂度:O(n^3);空间复杂度:O(n^2);

任意节点i到j的最短路径两种可能:

 

081028t67l8vd73686e68m.png

        暑假,小哼准备去一些城市旅游。有些城市之间有公路,有些城市之间则没有,如下图。为了节省经费以及方便计划旅程,小哼希望在出发之前知道任意两个城市之前的最短路程。

081028xjgvimgz7882qdu7.png

        上图中有4个城市8条公路,公路上的数字表示这条公路的长短。请注意这些公路是单向的。我们现在需要求任意两个城市之间的最短路程,也就是求任意两个点之间的最短路径。这个问题这也被称为“多源最短路径”问题。

        现在需要一个数据结构来存储图的信息,我们仍然可以用一个4*4的矩阵(二维数组e)来存储。比如1号城市到2号城市的路程为2,则设e[1][2]的值为2。2号城市无法到达4号城市,则设置e[2][4]的值为∞。另外此处约定一个城市自己是到自己的也是0,例如e[1][1]为0,具体如下。

081028o2n5ebn8hdeh9e5l.png

        现在回到问题:如何求任意两点之间最短路径呢?通过之前的学习我们知道通过深度或广度优先搜索可以求出两点之间的最短路径。所以进行n2遍深度或广度优先搜索,即对每两个点都进行一次深度或广度优先搜索,便可以求得任意两点之间的最短路径。可是还有没有别的方法呢?

        我们来想一想,根据我们以往的经验,如果要让任意两点(例如从顶点a点到顶点b)之间的路程变短,只能引入第三个点(顶点k),并通过这个顶点k中转即a->k->b,才可能缩短原来从顶点a点到顶点b的路程。那么这个中转的顶点k是1~n中的哪个点呢?甚至有时候不只通过一个点,而是经过两个点或者更多点中转会更短,即a->k1->k2b->或者a->k1->k2…->k->i…->b。比如上图中从4号城市到3号城市(4->3)的路程e[4][3]原本是12。如果只通过1号城市中转(4->1->3),路程将缩短为11(e[4][1]+e[1][3]=5+6=11)。其实1号城市到3号城市也可以通过2号城市中转,使得1号到3号城市的路程缩短为5(e[1][2]+e[2][3]=2+3=5)。所以如果同时经过1号和2号两个城市中转的话,从4号城市到3号城市的路程会进一步缩短为10。通过这个的例子,我们发现每个顶点都有可能使得另外两个顶点之间的路程变短。好,下面我们将这个问题一般化。

当任意两点之间不允许经过第三个点时,这些城市之间最短路程就是初始路程,如下。

081029zdxxq919ttqt8tu8.png

         如现在只允许经过1号顶点,求任意两点之间的最短路程,应该如何求呢?只需判断e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是从i号顶点到j号顶点之间的路程。e[i][1]+e[1][j]表示的是从i号顶点先到1号顶点,再从1号顶点到j号顶点的路程之和。其中i是1~n循环,j也是1~n循环,代码实现如下。

  1. for(i=1;i<=n;i++)   
       for(j=1;j<=n;j++)   
          if( e[i][j] > e[i][1]+e[1][j] ) e[i][j] = e[i][1]+e[1][j];   
         
    

    在只允许经过1号顶点的情况下,任意两点之间的最短路程更新为:

081029itl7z7m4l9qqg56d.png

        通过上图我们发现:在只通过1号顶点中转的情况下,3号顶点到2号顶点(e[3][2])、4号顶点到2号顶点(e[4][2])以及4号顶点到3号顶点(e[4][3])的路程都变短了。

        接下来继续求在只允许经过1和2号两个顶点的情况下任意两点之间的最短路程。如何做呢?我们需要在只允许经过1号顶点时任意两点的最短路程的结果下,再判断如果经过2号顶点是否可以使得i号顶点到j号顶点之间的路程变得更短。即判断e[i][2]+e[2][j]是否比e[i][j]要小,代码实现为如下。

//经过1号顶点? ?
for(i=1;i<=n;i++)
   for(j=1;j<=n;j++)
      if(e[i][j]>e[i][1]+e[1][j])e[i][j]=e[i][1]+e[1][j];
//经过2号顶点? ?
for(i=1;i<=n;i++)
   for(j=1;j<=n;j++)
        if(e[i][j]>e[i][2]+e[2][j])e[i][j]=e[i][2]+e[2][j];

        在只允许经过1和2号顶点的情况下,任意两点之间的最短路程更新为:

081029e7gjlaaul4zk7z4n.png

        通过上图得知,在相比只允许通过1号顶点进行中转的情况下,这里允许通过1和2号顶点进行中转,使得e[1][3]和e[4][3]的路程变得更短了。

         同理,继续在只允许经过1、2和3号顶点进行中转的情况下,求任意两点之间的最短路程。任意两点之间的最短路程更新为:

081029pd747o8o87o07o7l.png

         最后允许通过所有顶点作为中转,任意两点之间最终的最短路程为:

081030h7tmht7cs2h7qftu.png

         整个算法过程虽然说起来很麻烦,但是代码实现却非常简单,核心代码只有五行:

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]) e[i][j]=e[i][k]+e[k][j]; 

        这段代码的基本思想就是:最开始只允许经过1号顶点进行中转,接下来只允许经过1和2号顶点进行中转……允许经过1~n号所有顶点进行中转,求任意两点之间的最短路程。用一句话概括就是:从i号顶点到j号顶点只经过前k号点的最短路程。其实这是一种“动态规划”的思想,下面给出这个算法的完整代码:

#include <stdio.h>   
int main()   
{   
    int e[10][10],k,i,j,n,m,t1,t2,t3;   
    int inf=99999999; //用inf(infinity的缩写)存储一个我们认为的正无穷值   
    //读入n和m,n表示顶点个数,m表示边的条数   
    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;   
    }     
    //Floyd-Warshall算法核心语句   
    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] )    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("\n");   
    }   
} 

        有一点需要注意的是:如何表示正无穷。我们通常将正无穷定义为99999999,因为这样即使两个正无穷相加,其和仍然不超过int类型的范围(C语言int类型可以存储的最大正整数是2147483647)。在实际应用中最好估计一下最短路径的上限,只需要设置比它大一点既可以。例如有100条边,每条边不超过100的话,只需将正无穷设置为10001即可。如果你认为正无穷和其它值相加得到一个大于正无穷的数是不被允许的话,我们只需在比较的时候加两个判断条件就可以了,请注意下面代码中带有下划线的语句。

//Floyd-Warshall算法核心语句   
for(k=1;k<=n;k++)   
  for(i=1;i<=n;i++)   
      for(j=1;j<=n;j++)   
         if(e[i][k]<inf && e[k][j]<inf && e[i][j]>e[i][k]+e[k][j])
            e[i][j]=e[i][k]+e[k][j]; 

上面代码的输入数据样式为:

4 8   
1 2 2   
1 3 6   
1 4 4   
2 3 3   
3 1 7   
3 4 1   
4 1 5   
4 3 12 

第一行两个数为n和m,n表示顶点个数,m表示边的条数。

接下来m行,每一行有三个数t1、t2 和t3,表示顶点t1到顶点t2的路程是t3。

得到最终结果如下:

081030is22w3mmnz3r33m3.png

通过这种方法我们可以求出任意两个点之间最短路径。它的时间复杂度是O(N3)。令人很震撼的是它竟然只有五行代码,实现起来非常容易。正是因为它实现起来非常容易,如果时间复杂度要求不高,使用Floyd-Warshall来求指定两点之间的最短路或者指定一个点到其余各个顶点的最短路径也是可行的。当然也有更快的算法。

另外需要注意的是:Floyd-Warshall算法不能解决带有“负权回路”(或者叫“负权环”)的图,因为带有“负权回路”的图没有最短路。例如下面这个图就不存在1号顶点到3号顶点的最短路径。因为1->2->3->1->2->3->…->1->2->3这样路径中,每绕一次1->-2>3这样的环,最短路就会减少1,永远找不到最短路。其实如果一个图中带有“负权回路”那么这个图则没有最短路。

081030elthvel6et6k886y.png

此算法由Robert W. Floyd(罗伯特·弗洛伊德)于1962年发表在“Communications of the ACM”上。同年Stephen Warshall(史蒂芬·沃舍尔)也独立发表了这个算法。Robert W.Floyd这个牛人是朵奇葩,他原本在芝加哥大学读的文学,但是因为当时美国经济不太景气,找工作比较困难,无奈之下到西屋电气公司当了一名计算机操作员,在IBM650机房值夜班,并由此开始了他的计算机生涯。此外他还和J.W.J. Williams(威廉姆斯)于1964年共同发明了著名的堆排序算法HEAPSORT。Robert W.Floyd在1987年获得了图灵奖。

 

例1:医院设置

题目描述

设有一棵二叉树,如图:

其中,圈中的数字表示结点中居民的人口。圈边上数字表示结点编号,现在要求在某个结点上建立一个医院,使所有居民所走的路程之和为最小,同时约定,相邻接点之间的距离为1。如上图中,

若医院建在1 处,则距离和=4+12+2*20+2*40=136;若医院建在3 处,则距离和=4*2+13+20+40=81……

输入格式

第一行一个整数n,表示树的结点数。(n≤100)

接下来的n行每行描述了一个结点的状况,包含三个整数,整数之间用空格(一个或多个)分隔,其中:第一个数为居民人口数;第二个数为左链接,为0表示无链接;第三个数为右链接。

输出格式

一个整数,表示最小距离和。

输入输出样例

输入 

5						
13 2 3
4 0 0
12 4 5
20 0 0
40 0 0

输出 

81

#include<bits/stdc++.h>
#define big 100000007
int p[101],dis[101][101],sum;
int n,l,r;
using namespace std;
int main()
{   cin>>n;
    memset(dis,big,sizeof(dis));
    for(int i=1;i<=n;i++)
    {   dis[i][i]=0;
        cin>>p[i];
        cin>>l>>r;
        if(l>=0) dis[i][l]=1;dis[l][i]=1;  //注意双向路
        if(r>=0) dis[i][r]=1;dis[r][i]=1;//同上
    }
    for(int k=1;k<=n;k++)
    {   for(int i=1;i<=n;i++)
        {   if(i!=k)
            {   for(int j=1;j<=n;j++)
                  if(         ) dis[i][j]=              ;//求最短路                
            }
        }
    }
    int minn=big;
    for(int i=1;i<=n;i++)
    {   sum=0;
        for(int j=1;j<=n;j++)
            if(       ) sum+=           ;  //注意i不等于j,根据题意计算
        if(minn>sum) minn=sum;
    }
    cout<<minn<<endl;
    return 0;
}

 

例2、最短路径问题

【问题描述】
平面上有n个点(n<=100),每个点的坐标均在-10000~10000之间。其中的一些点之间有连线。若有连线,则表示可从一个点到达另一个点,即两点间有通路,通路的距离为两点间的直线距离。现在的任务是找出从一点到另一点之间的最短路径。
【输入格式】
输入文件为short.in,共n+m+3行,其中:
第一行为整数n。
第2行到第n+1行(共n行) ,每行两个整数x和y,描述了一个点的坐标。
第n+2行为一个整数m,表示图中连线的个数。
此后的m 行,每行描述一条连线,由两个整数i和j组成,表示第i个点和第j个点之间有连线。
最后一行:两个整数s和t,分别表示源点和目标点。
【输出格式】
输出文件为short.out,仅一行,一个实数(保留两位小数),表示从s到t的最短路径长度。
【输入样例】
5
0 0
2 0
2 2
0 2
3 1 
5
1 2
1 3
1 4
2 5
3 5
1 5
 
【输出样例】
3.41
#include<bits/stdc++.h>
using namespace std;
int a[101][3];
double f[101][101];
int n,i,j,k,x,y,m,s,e;
int main()
{  freopen("short.in","r",stdin);
   freopen("short.out","w",stdout);
   cin >> n;
   for (i = 1; i <= n; i++)   cin>>a[i][1]>>a[i][2];
   cin >> m;
   memset(f,0x7f,sizeof(f));     //初始化f数组为最大值
   for (i = 1; i <= m; i++)      //预处理出x、y间距离
   {  cin >> x >> y;
      f[y][x]=f[x][y]=sqrt(pow(double(a[x][1]-a[y][1]),2)+pow(double(a[x][2]-a[y][2]),2));                        //pow(x,y)表示x^y,其中x,y必须为double类型,要用cmath库
   }
   cin >> s >> e;
   for (k = 1; k <= n; k++)     //floyed 最短路算法
       for (i = 1; i <= n; i++)
          for (j = 1; j <= n; j++)
             if ((i != j) && (i != k) && (j != k) && (f[i][k]+f[k][j] < f[i][j]))
                 f[i][j] = f[i][k] + f[k][j];
   printf("%.2lf\n",f[s][e]);
   return 0;
}

例3、牛的旅行

【问题描述】
  农民John的农场里有很多牧区。有的路径连接一些特定的牧区。一片所有连通的牧区称为一个牧场。但是就目前而言,你能看到至少有两个牧区不连通。现在,John想在农场里添加一条路径 ( 注意,恰好一条 )。对这条路径有这样的限制:一个牧场的直径就是牧场中最远的两个牧区的距离 ( 本题中所提到的所有距离指的都是最短的距离 )。考虑如下的两个牧场,图1是有5个牧区的牧场,牧区用“*”表示,路径用直线表示。每一个牧区都有自己的坐标:
 

        图1所示的牧场的直径大约是12.07106, 最远的两个牧区是A和E,它们之间的最短路径是A-B-E。
        这两个牧场都在John的农场上。John将会在两个牧场中各选一个牧区,然后用一条路径连起来,使得连通后这个新的更大的牧场有最小的直径。注意,如果两条路径中途相交,我们不认为它们是连通的。只有两条路径在同一个牧区相交,我们才认为它们是连通的。
  现在请你编程找出一条连接两个不同牧场的路径,使得连上这条路径后,这个更大的新牧场有最小的直径。
【输入格式】
     第 1 行:一个整数N (1 <= N <= 150), 表示牧区数;
     第 2 到 N+1 行:每行两个整数X,Y ( 0 <= X,Y<= 100000 ), 表示N个牧区的坐标。每个牧区的坐标都是不一样的。
     第 N+2 行到第 2*N+1 行:每行包括N个数字 ( 0或1 ) 表示一个对称邻接矩阵。
     例如,题目描述中的两个牧场的矩阵描述如下:
A B C D E F G H      
A 0 1 0 0 0 0 0 0      
B 1 0 1 1 1 0 0 0      
C 0 1 0 0 1 0 0 0      
D 0 1 0 0 1 0 0 0      
E 0 1 1 1 0 0 0 0      
F 0 0 0 0 0 0 1 0      
G 0 0 0 0 0 1 0 1      
H 0 0 0 0 0 0 1 0   
输入数据中至少包括两个不连通的牧区。
【输出格式】
   只有一行,包括一个实数,表示所求答案。数字保留六位小数。
【输入样例】
   8
   10 10
   15 10
   20 10
   15 15
   20 15
   30 15
   25 10
   30 10
   01000000
   10111000
   01001000
   01001000
   01110000
   00000010
   00000101
   00000010
【输出样例】
   22.071068
【算法分析】
    用Floyed求出任两点间的最短路,然后求出每个点到所有可达的点的最大距离,记做mdis[i]。(Floyed算法)
      r1=max(mdis[i])
   然后枚举不连通的两点i,j,把他们连通,则新的直径是mdis[i]+mdis[j]+(i,j)间的距离。
   r2=min(mdis[i]+mdis[j]+dis[i,j])
   re=max(r1,r2)
   re就是所求。
#include<bits/stdc++.h>
using namespace std;
double f[151][151],m[151],minx,r,temp,x[151],y[151],maxint=1e12;
double dist(int i,int j)
{  return sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j])) ;  
}
int main()
{ int i,j,n,k;char c;
  cin>>n;
  for(i=1;i<=n;i++)cin>>x[i]>>y[i];
  for(i=1;i<=n;i++)
   for(j=1;j<=n;j++)
    { cin>>c;
      if(c=='1')f[i][j]=dist(i,j);
      else f[i][j]=maxint;
    }
  for(k=1;k<=n;k++)
    for(i=1;i<=n;i++)
      for(j=1;j<=n;j++)
        if(i!=j&&i!=k&&j!=k)
           if(f[i][k]<maxint-1&&f[k][j]<maxint-1)
              if(f[i][j]>f[i][k]+f[k][j])
                f[i][j]=f[i][k]+f[k][j];
  memset(m,0,sizeof(m));
  for(i=1;i<=n;i++)
    for(j=1;j<=n;j++)
      if(f[i][j]<maxint-1&&m[i]<f[i][j])m[i]=f[i][j]; 
  minx=1e20;
  for(i=1;i<=n;i++)
   for(j=1;j<=n;j++)
    if(i!=j&&f[i][j]>maxint-1)
    { temp=dist(i,j);
      if(minx>m[i]+m[j]+temp)minx=m[i]+m[j]+temp;
    }
  r=0;
  for(i=1;i<=n;i++)if (m[i]>minx)minx=m[i];
  printf("%.6lf",minx);
  return 0;
}

 

 
 

二、迪杰斯特拉(Dijkstra)算法介绍

用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。
Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。
算法描述:
       设起点为s,dis[v]表示从s到v的最短路径,pre[v]为v的前驱节点,用来输出路径。
       a)初始化:dis[v]=∞(v≠s); dis[s]=0; pre[s]=0;
       b)For (i = 1; i <= n ; i++)
            1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。
            2.u标记为已确定最短路径
            3.For 与u相连的每个未确定最短路径的顶点v
              if  (dis[u]+w[u][v] < dis[v])
               {
                  dis[v] = dis[u] + w[u][v];
                  pre[v] = u;
               }
         c)算法结束: dis[v] s v 的最短距离; pre[v] v的前驱节点,用来输出路径。

1、算法特点:

迪科斯彻算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。

2、算法的思路

Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各个顶点的最短距离和一个保存已经找到了最短路径的顶点的集合:T,初始时,原点 s 的路径权重被赋为 0 (dis[s] = 0)。若对于顶点 s 存在能直接到达的边(s,m),则把dis[m]设为w(s, m),同时把所有其他(s不能直接到达的)顶点的路径长度设为无穷大。初始时,集合T只有顶点s。
然后,从dis数组选择最小值,则该值就是源点s到该值对应的顶点的最短路径,并且把该点加入到T中,OK,此时完成一个顶点,然后,我们需要看看新加入的顶点是否可以到达其他顶点并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在dis中的值。然后,又从dis中找出最小值,重复上述动作,直到T中包含了图的所有顶点。

3、Dijkstra算法示例演示

下面我求下图,从顶点v1到其他各个顶点的最短路径

这里写图片描述

首先第一步,我们先声明一个dis数组,该数组初始化的值为:
这里写图片描述

我们的顶点集T的初始化为:T={v1}

既然是求 v1顶点到其余各个顶点的最短路程,那就先找一个离 1 号顶点最近的顶点。通过数组 dis 可知当前离v1顶点最近是 v3顶点。当选择了 2 号顶点后,dis[2](下标从0开始)的值就已经从“估计值”变为了“确定值”,即 v1顶点到 v3顶点的最短路程就是当前 dis[2]值。将V3加入到T中。
为什么呢?因为目前离 v1顶点最近的是 v3顶点,并且这个图所有的边都是正数,那么肯定不可能通过第三个顶点中转,使得 v1顶点到 v3顶点的路程进一步缩短了。因为 v1顶点到其它顶点的路程肯定没有 v1到 v3顶点短.

OK,既然确定了一个顶点的最短路径,下面我们就要根据这个新入的顶点V3会有出度,发现以v3 为弧尾的有: < v3,v4 >,那么我们看看路径:v1–v3–v4的长度是否比v1–v4短,其实这个已经是很明显的了,因为dis[3]代表的就是v1–v4的长度为无穷大,而v1–v3–v4的长度为:10+50=60,所以更新dis[3]的值,得到如下结果:
这里写图片描述

因此 dis[3]要更新为 60。这个过程有个专业术语叫做“松弛”。即 v1顶点到 v4顶点的路程即 dis[3],通过 < v3,v4> 这条边松弛成功。这便是 Dijkstra 算法的主要思想:通过“边”来松弛v1顶点到其余各个顶点的路程。

然后,我们又从除dis[2]和dis[0]外的其他值中寻找最小值,发现dis[4]的值最小,通过之前是解释的原理,可以知道v1到v5的最短距离就是dis[4]的值,然后,我们把v5加入到集合T中,然后,考虑v5的出度是否会影响我们的数组dis的值,v5有两条出度:< v5,v4>和 < v5,v6>,然后我们发现:v1–v5–v4的长度为:50,而dis[3]的值为60,所以我们要更新dis[3]的值.另外,v1-v5-v6的长度为:90,而dis[5]为100,所以我们需要更新dis[5]的值。更新后的dis数组如下图:
这里写图片描述

然后,继续从dis中选择未确定的顶点的值中选择一个最小的值,发现dis[3]的值是最小的,所以把v4加入到集合T中,此时集合T={v1,v3,v5,v4},然后,考虑v4的出度是否会影响我们的数组dis的值,v4有一条出度:< v4,v6>,然后我们发现:v1–v5–v4–v6的长度为:60,而dis[5]的值为90,所以我们要更新dis[5]的值,更新后的dis数组如下图:
这里写图片描述

然后,我们使用同样原理,分别确定了v6和v2的最短路径,最后dis的数组的值如下:
这里写图片描述

因此,从图中,我们可以发现v1-v2的值为:∞,代表没有路径从v1到达v2。所以我们得到的最后的结果为:

起点  终点    最短路径    长度
v1    v2     无          ∞    
      v3     {v1,v3}    10
      v4     {v1,v5,v4}  50
      v5     {v1,v5}    30
      v6     {v1,v5,v4,v6} 60

 

 想必大家一定会Floyd了吧,Floyd只要暴力的三个for就可以出来,代码好背,也好理解,但缺点就是时间复杂度高是O(n³)。

   于是今天就给大家带来一种时间复杂度是O(n²),的算法:Dijkstra(迪杰斯特拉)。

   这个算法所求的是单源最短路,好比说你写好了Dijkstra的函数,那么只要输入点a的编号,就可算出图上每个点到这个点的距离。

  我先上一组数据(这是无向图):

5 6
1 2 5
1 3 8
2 3 1
2 4 3
4 5 7
2 5 2

 

图大概是这个样子:

Dijkstra 算法是一种类似于贪心的算法,步骤如下:

1、当到一个时间点时,图上部分的点的最短距离已确定,部分点的最短距离未确定。

2、选一个所有未确定点中离源点最近的点,把他认为成最短距离。

3、再把这个点所有出边遍历一边,更新所有的点。

下面模拟一下:

我们以1为源点,来求所有点到一号点的最短路径。

先建立一个dis数组,dis[i]表示第i号点到源点(1号点)的估计值,你可能会问为什么是估计值,因为这个估计值会不断更新,更新到一定次数就变成答案了,这个我们一会再说。

然后我们在建立一个临界矩阵,叫做:map,map[i][j]=v表示从i到j这条边的权值是v。

dis初始值除了源点本身都是无穷大。源点本身都是0.

先从1号点开始。一号点,map[1][2]=5,一号点离2号点是5,比无穷大要小,所以dis[2]从无穷大变成了5。顺便,我们用minn记录距离1号点最短的点,留着以后会用。

dis[0,5,∞,∞,∞]。minn=2。

然后搜到3号点,map[1][3]=8,距离是8,比原来的dis[3]的∞小,于是dis[3]=8。但是8比dis[2]的5要大,所以minn不更新。

dis[0,5,8,∞,∞]

接着分别搜索4,5号点,发现map[1][4],map[1][5]都是∞,所以就不更新。

现在,dis数组所呈现的明显不是最终答案,因为我们才更新一遍,现在我们开始第二次更新,第二次更新以什么为开始呢?就是以上一次我们存下来的,minn,相当于把2当源点,求所有点到它的最短路,加上它到真正的源点(1号点)的距离,就是我们要求的最短路。

从2号点开始,搜索3号点,map[2][3]=1,原本dis[3]=8,发现dis[2]+map[2][3]=5+1=6<dis[3](8)所以更新dis[3]为6,minn=3

dis[0,5,6,∞,∞] minn=3.

然后搜索4号点,map[2][4]=3,原本dis[4]=∞,所以,dis[2]+map[2][4]=5+3=8<dis[4](∞)所以更新dis[4]=8,因为map[2][4]=3,3>1,minn不更新。

dis[0,5,6,8,∞] minn=3.

接着搜索5号点,map[2][5]=2,5+2=7,7<∞,dis[5]=7minn不变。

dis[0,5,6,8,7]

二号点搜完,因为minn是3,继续搜索3号点。

三号点还是按照二号点的方法搜索,发现没有可以更新的,然后搜索四号。

四号搜5号点,发现8+7>5+2,所以依然不更新,然后跳出循环。

 现在的估计值就全部为确定值了:

dis[0,5,6,8,7]

这就是每个点到源点一号点的距离,我们来看一下代码:

 

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
int map[110][110];//这就是map数组,存储图
int dis[10010];//dis数组,存储估计值
int book[10010];//book[i]代表这个点有没有被当做源点去搜索过,1为有,0为没有。这样就不会重复搜索了。
int n,m;
void dijkstra(int u)//主函数,参数是源点编号
{
    memset(dis,88,sizeof(dis));//把dis数组附最大值(88不是十进制的88,其实很大)
    int start=u;//先从源点搜索
    book[start]=1;//标记源点已经搜索过
    for(int i=1;i<=n;i++)
    {
        dis[i]=min(dis[i],map[start][i]);//先更新一遍
    }
    for(int i=1;i<=n-1;i++)
    {
        int minn=9999999;//谢评论区,改正一下:这里的minn不是题解上的minn,这代表的是最近点到源点的距离,start才代表最近的点、
        for(int j=1;j<=n;j++)
            if(book[j]==0 && minn>dis[j])
            {
                minn=dis[j];
                start=j;//找到离源点最近的点,然后把编号记录下来,用于搜索。
            }
        book[start]=1;        
        for(int j=1;j<=n;j++)
            dis[j]=min(dis[j],dis[start]+map[start][j]);//以新的点来更新dis。
    }
}
int main()
{
    cin>>n>>m;
    memset(map,88,sizeof(map));
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        map[a][b]=c;
    }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i==j)
                map[i][j]=0;
    dijkstra(1);//以1为源点。
    for(int i=1;i<=n;i++)
        cout<<dis[i]<<" ";
}

 

这就是用邻接矩阵实现dijkstra,但是这个算法有一个坏处,就是出现负权边,这个算法就炸了,要解决负权边,我以后会给大家带来Bell man ford(SPFA)

这个算法的复杂度是O(n²),空间复杂度也是n平方,如果用邻接表来实现,最差情况,时间复杂度是O(n*m)似乎比n²要大一些,但是空间复杂度会从n平方变成m,少了很多,现在我呈上邻接表的代码。

 

#include <iostream>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <cstdlib>
using namespace std;
int value[10010],to[10010],next[10010];
int head[10010],total;
int book[10010];
int dis[10010];
int n,m;
void adl(int a,int b,int c)
{
    total++;
    to[total]=b;
    value[total]=c;
    next[total]=head[a];
    head[a]=total;
}
void dijkstra(int u)
{
    memset(dis,88,sizeof(dis));
    memset(book,0,sizeof(book));
    dis[u]=0;
    for(int i=1;i<=n;i++)
    {
        int start=-1;
        for(int j=1;j<=n;j++)
            if(book[j]==0 && (dis[start]>dis[j] || start==-1))
                start=j;
        book[start]=1;
        for(int e=head[start];e;e=next[e])
            dis[to[e]]=min(dis[to[e]],dis[start]+value[e]);
    }
}
int main()
{
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        adl(a,b,c);
     } 
     dijkstra(1);
     for(int i=1;i<=n;i++)
         cout<<dis[i]<<" ";
}

 

 

一年多了,身为一个OIer,经历了太多。

当年那么畏惧的Dijkstra、邻接表,现在已经是信手拈来。

那个暑假,因为Djkstra名字的朗朗上口,讲自己名字改为了Dijkstra,但是逐渐因为SPFA的可处理负权边,也将Dijkstra,淡忘。

如今突然想起,加入了堆优化,有人说:一道题如果边权没有负数,那么一定是在卡SPFA。这时候就用到了堆优化的Dijkstra。

一年前提到,朴素的Dijkstra时间复杂度是n^2,被SPFA的m*常数吊打,但是,经过堆优化,Dijkstra的时间复杂度能达到nlogn,如果这个图特别稠密的话,也就是m特别大(比如完全图就是n^2),那么nlogn是要小于m的,这就用到了Dijkstra

首先堆优化怎么优化?观察上面的代码,每次循环中都再嵌套一个循环求dis值最小的点。这里,我们可以用一个优先队列,每当搜索到一个新点,扔到优先队列里面,这样每次就取队首的绝对是最优值。这样可以省去for循环。

 

#include <bits/stdc++.h>
#define in(a) a=read()
#define REP(i,k,n) for(long long i=k;i<=n;i++)
#define MAXN 10010
using namespace std;
typedef pair<long long,long long> P;
inline long long read(){
    long long x=0,t=1,c;
    while(!isdigit(c=getchar())) if(c=='-') t=-1;
    while(isdigit(c)) x=x*10+c-'0',c=getchar();
    return x*t;
}
long long n,m,s;
long long total=0,head[MAXN],nxt[MAXN<<10],to[MAXN<<10],val[MAXN<<10];
long long dis[MAXN],vis[MAXN];
priority_queue <P, vector<P>,greater<P> > Q;//优先队列优化
inline void adl(long long a,long long b,long long c){
    total++;
    to[total]=b;
    val[total]=c;
    nxt[total]=head[a];
    head[a]=total;
    return ;
}
inline void Dijkstra(){
    REP(i,1,n)  dis[i]=2147483647;
    dis[s]=0;
    Q.push(P(0,s));
    while(!Q.empty()){
        long long u=Q.top().second;//取出dis最小的点
        Q.pop();//弹出
        if(vis[u])  continue;
        vis[u]=1;
        for(long long e=head[u];e;e=nxt[e])
            if(dis[to[e]]>dis[u]+val[e]){
                dis[to[e]]=dis[u]+val[e];
                Q.push(P(dis[to[e]],to[e]));//插入
            }
    }
    return ;
}
int main(){
    in(n),in(m),in(s);
    long long a,b,c;
    REP(i,1,m)  in(a),in(b),in(c),adl(a,b,c);
    Dijkstra();
    REP(i,1,n)  printf("%lld ",dis[i]);
}

 

 

  • 7
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值