最短路径算法详细介绍

据 Drew 所知最短路经算法现在重要的应用有计算机网络路由算法,机器人探路,交通路线导航,人工智能,游戏设计等等。美国火星探测器核心的寻路算法就是采用的D*(D Star)算法。

    最短路经计算分静态最短路计算和动态最短路计算。

    静态路径最短路径算法是外界环境不变,计算最短路径。主要有Dijkstra算法,A*(A Star)算法。 

    动态路径最短路是外界环境不断发生变化,即不能计算预测的情况下计算最短路。如在游戏中敌人或障碍物不断移动的情况下。典型的有D*算法。

 

这是Drew程序实现的10000个节点的随机路网三条互不相交最短路

 

真实路网计算K条路径示例:节点5696到节点3006,三条最快速路,可以看出路径基本上走环线或主干路。黑线为第一条,兰线为第二条,红线为第三条。约束条件系数为1.2。共享部分路段。 显示计算部分完全由Drew自己开发的程序完成。

 

  参见 K条路算法测试程序

Dijkstra算法求最短路径:

Dijkstra算法是典型最短路算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法能得出最短路径的最优解,但由于它遍历计算的节点很多,所以效率低。

Dijkstra算法是很有代表性的最短路算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。

Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表方式,Drew为了和下面要介绍的 A* 算法和 D* 算法表述一致,这里均采用OPEN,CLOSE表的方式。

大概过程:
创建两个表,OPEN, CLOSE。
OPEN表保存所有已生成而未考察的节点,CLOSED表中记录已访问过的节点。
1. 访问路网中里起始点最近且没有被检查过的点,把这个点放入OPEN组中等待检查。
2. 从OPEN表中找出距起始点最近的点,找出这个点的所有子节点,把这个点放到CLOSE表中。
3. 遍历考察这个点的子节点。求出这些子节点距起始点的距离值,放子节点到OPEN表中。
4. 重复2,3,步。直到OPEN表为空,或找到目标点。

这是在drew 程序中4000个节点的随机路网上Dijkstra算法搜索最短路的演示,黑色圆圈表示经过遍历计算过的点由图中可以看到Dijkstra算法从起始点开始向周围层层计算扩展,在计算大量节点后,到达目标点。所以速度慢效率低。

提高Dijkstra搜索速度的方法很多,据Drew所知,常用的有数据结构采用Binary heap的方法,和用Dijkstra从起始点和终点同时搜索的方法。

推荐网页:http://www.cs.ecnu.edu.cn/assist/js04/ZJS045/ZJS04505/zjs045050a.htm

简明扼要介绍Dijkstra算法,有图解显示和源码下载。

A*(A Star)算法:启发式(heuristic)算法

A*(A-Star)算法是一种静态路网中求解最短路最有效的方法。

公式表示为:        f(n)=g(n)+h(n), 
其中f(n) 是节点n从初始点到目标点的估价函数,
g(n) 是在状态空间中从初始节点到n节点的实际代价,
h(n)是从n到目标节点最佳路径的估计代价。

保证找到最短路径(最优解的)条件,关键在于估价函数h(n)的选取:
估价值h(n)<= n到目标节点的距离实际值,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。
如果 估价值>实际值, 搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。
估价值与实际值越接近,估价函数取得就越好。
例如对于几何路网来说,可以取两节点间欧几理德距离(直线距离)做为估价值,即f=g(n)+sqrt((dx-nx)*(dx-nx)+(dy-ny)*(dy-ny));这样估价函数f在g值一定的情况下,会或多或少的受估价值h的制约,节点距目标点近,h值小,f值相对就小,能保证最短路的搜索向终点的方向进行。明显优于Dijstra算法的毫无无方向的向四周搜索。

conditions of heuristic
Optimistic (must be less than or equal to the real cost)
As close to the real cost as possible

主要搜索过程:
创建两个表,OPEN表保存所有已生成而未考察的节点,CLOSED表中记录已访问过的节点。
遍历当前节点的各个节点,将n节点放入CLOSE中,取n节点的子节点X,->算X的估价值->
While(OPEN!=NULL)
{
从OPEN表中取估价值f最小的节点n;
if(n节点==目标节点) break;
else
{
if(X in OPEN) 比较两个X的估价值f //注意是同一个节点的两个不同路径的估价值
if( X的估价值小于OPEN表的估价值 )
   更新OPEN表中的估价值; //取最小路径的估价值

if(X in CLOSE) 比较两个X的估价值 //注意是同一个节点的两个不同路径的估价值
if( X的估价值小于CLOSE表的估价值 )
   更新CLOSE表中的估价值; 把X节点放入OPEN //取最小路径的估价值

if(X not in both)
求X的估价值;
   并将X插入OPEN表中; //还没有排序
}

将n节点插入CLOSE表中;
按照估价值将OPEN表中的节点排序; //实际上是比较OPEN表内节点f的大小,从最小路径的节点向下进行。
}

上图是和上面Dijkstra算法使用同一个路网,相同的起点终点,用A*算法的情况,计算的点数从起始点逐渐向目标点方向扩展,计算的节点数量明显比Dijkstra少得多,效率很高,且能得到最优解。

A*算法和Dijistra算法的区别在于有无估价值,Dijistra算法相当于A*算法中估价值为0的情况。

 

推荐文章链接:

Amit 斯坦福大学一个博士的游戏网站,上面有关于A*算法介绍和不少有价值的链接    http://theory.stanford.edu/~amitp/GameProgramming/

Sunway写的两篇很好的介绍启发式和A*算法的中文文章并有A*源码下载:

初识A*算法 http://creativesoft.home.shangdu.net/AStart1.htm

深入A*算法 http://creativesoft.home.shangdu.net/AStart2.htm

需要注意的是Sunway上面文章“深入A*算法”中引用了一个A*的游戏程序进行讲解,并有这个源码的下载,不过它有一个不小的Bug, 就是新的子节点放入OPEN表中进行了排序,而当子节点在Open表和Closed表中时,重新计算估价值后,没有重新的对Open表中的节点排序,这个问题会导致计算有时得不到最优解,另外在路网权重悬殊很大时,搜索范围不但超过Dijkstra,甚至搜索全部路网, 使效率大大降低。 

Drew 对这个问题进行了如下修正,当子节点在Open表和Closed表中时,重新计算估价值后,删除OPEN表中的老的节点,将有新估价值的节点插入OPEN表中,重新排序,经测试效果良好,修改的代码如下,红色部分为Drew添加的代码.添加进程序的相应部分即可。

在函数GenerateSucc()中 
...................................
g=BestNode->g+1; /* g(Successor)=g(BestNode)+cost of getting from BestNode to Successor */
TileNumS=TileNum((int)x,(int)y); /* identification purposes */
if ((Old=CheckOPEN(TileNumS)) != NULL) 

for(c=0;c<8;c++)
if(BestNode->Child[c] == NULL) /* Add Old to the list of BestNode's Children (or Successors). */
break;
BestNode->Child[c]=Old;

if (g < Old->g) 
{
Old->Parent=BestNode;
Old->g=g;
Old->f=g+Old->h;

 
//Drew 在该处添加如下红色代码 
//Implement by Drew 
NODE *q,*p=OPEN->NextNode, *temp=OPEN->NextNode;
while(p!=NULL && p->NodeNum != Old->NodeNum)
{
    q=p;
    p=p->NextNode;
}
if(p->NodeNum == Old->NodeNum)
{
   if(p==OPEN->NextNode)
  {
     temp = temp->NextNode;
     OPEN ->NextNode = temp;
  }
  else
  q->NextNode = p->NextNode;
 }
Insert(Old); // Insert Successor on OPEN list wrt f 

...................................................... 

 

另一种A*(A Star)算法:

这种算法可以不直接用估价值,直接用Dijkstra算法程序实现A*算法,Drew对它进行了测试,达到和A*完全一样的计算效果,且非常简单。

以邻接矩阵为例,更改原来邻接矩阵i行j列元素Dij为 Dij+Djq-Diq; 起始点到目标点的方向i->j, 终点q. Dij为(i到j路段的权重或距离)

其中:Djq,Diq的作用相当于估价值 Djq=(j到q的直线距离);Diq=(i到q的直线距离)

原理:i 到q方向符合Dij+Djq > Diq ,取Dij+Djq-Diq 小,如果是相反方向Dij+Djq-Diq会很大。因此达到向目标方向寻路的作用。

 

动态路网,最短路径算法 D*

A* 在静态路网中非常有效(very efficient for static worlds),但不适于在动态路网,环境如权重等不断变化的动态环境下。 

D*是动态A*(D-Star,Dynamic A Star) 卡内及梅隆机器人中心的Stentz在1994和1995年两篇文章提出,主要用于机器人探路。是火星探测器采用的寻路算法。

Optimal and Efficient Path Planning for Partially-Known Environments

The Focussed D* Algorithm for Real-Time Replanning


主要方法(这些完全是Drew在读了上述资料和编制程序中的个人理解,不能保证完全正确,仅供参考):

1.先用Dijstra算法从目标节点G向起始节点搜索。储存路网中目标点到各个节点的最短路和该位置到目标点的实际值h,k(k为所有变化h之中最小的值,当前为k=h。每个节点包含上一节点到目标点的最短路信息1(2),2(5),5(4),4(7)。则1到4的最短路为1-2-5-4。
原OPEN和CLOSE中节点信息保存。

2.机器人沿最短路开始移动,在移动的下一节点没有变化时,无需计算,利用上一步Dijstra计算出的最短路信息从出发点向后追述即可,当在Y点探测到下一节点X状态发生改变,如堵塞。机器人首先调整自己在当前位置Y到目标点G的实际值h(Y),h(Y)=X到Y的新权值c(X,Y)+X的原实际值h(X).X为下一节点(到目标点方向Y->X->G),Y是当前点。k值取h值变化前后的最小。

3.用A*或其它算法计算,这里假设用A*算法,遍历Y的子节点,点放入CLOSE,调整Y的子节点a的h值,h(a)=h(Y)+Y到子节点a的权重C(Y,a),比较a点是否存在于OPEN和CLOSE中,方法如下:

while()
{
从OPEN表中取k值最小的节点Y;
遍历Y的子节点a,计算a的h值 h(a)=h(Y)+Y到子节点a的权重C(Y,a)
{
    if(a in OPEN)     比较两个a的h值 
    if( a的h值小于OPEN表a的h值 )
    {
     更新OPEN表中a的h值;k值取最小的h值
          有未受影响的最短路经存在
          break; 
    }
    if(a in CLOSE) 比较两个a的h值 //注意是同一个节点的两个不同路径的估价值
    if( a的h值小于CLOSE表的h值 )
    {
     更新CLOSE表中a的h值; k值取最小的h值;将a节点放入OPEN表
          有未受影响的最短路经存在
          break;
    }
    if(a not in both)
        将a插入OPEN表中; //还没有排序
}
放Y到CLOSE表;
OPEN表比较k值大小进行排序;
}
机器人利用第一步Dijstra计算出的最短路信息从a点到目标点的最短路经进行。

D*算法在动态环境中寻路非常有效,向目标点移动中,只检查最短路径上下一节点或临近节点的变化情况,如机器人寻路等情况。对于距离远的最短路径上发生的变化,则感觉不太适用。

上图是Drew在4000个节点的随机路网上做的分析演示,细黑线为第一次计算出的最短路,红点部分为路径上发生变化的堵塞点,当机器人位于982点时,检测到前面发生路段堵塞,在该点重新根据新的信息计算路径,可以看到圆圈点为重新计算遍历过的点,仅仅计算了很少得点就找到了最短路,说明计算非常有效,迅速。绿线为计算出的绕开堵塞部分的新的最短路径。

Dijkstra算法(单源最短路径)

      单源最短路径问题,即在图中求出给定顶点到其它任一顶点的最短路径。在弄清楚如何求算单源最短路径问题之前,必须弄清楚最短路径的最优子结构性质。

一.最短路径的最优子结构性质

   该性质描述为:如果P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,k和s是这条路径上的一个中间顶点,那么P(k,s)必定是从k到s的最短路径。下面证明该性质的正确性。

   假设P(i,j)={Vi....Vk..Vs...Vj}是从顶点i到j的最短路径,则有P(i,j)=P(i,k)+P(k,s)+P(s,j)。而P(k,s)不是从k到s的最短距离,那么必定存在另一条从k到s的最短路径P'(k,s),那么P'(i,j)=P(i,k)+P'(k,s)+P(s,j)<P(i,j)。则与P(i,j)是从i到j的最短路径相矛盾。因此该性质得证。

二.Dijkstra算法

   由上述性质可知,如果存在一条从i到j的最短路径(Vi.....Vk,Vj),Vk是Vj前面的一顶点。那么(Vi...Vk)也必定是从i到k的最短路径。为了求出最短路径,Dijkstra就提出了以最短路径长度递增,逐次生成最短路径的算法。譬如对于源顶点V0,首先选择其直接相邻的顶点中长度最短的顶点Vi,那么当前已知可得从V0到达Vj顶点的最短距离dist[j]=min{dist[j],dist[i]+matrix[i][j]}。根据这种思路,

假设存在G=<V,E>,源顶点为V0,U={V0},dist[i]记录V0到i的最短距离,path[i]记录从V0到i路径上的i前面的一个顶点。

1.从V-U中选择使dist[i]值最小的顶点i,将i加入到U中;

2.更新与i直接相邻顶点的dist值。(dist[j]=min{dist[j],dist[i]+matrix[i][j]})

3.知道U=V,停止。

代码实现:

/*Dijkstra求单源最短路径 */
 
#include <iostream>
#include<stack>
#define M 100
#define N 100
using namespace std;

typedef struct node
{
    int matrix[N][M];      //邻接矩阵
    int n;                 //顶点数
    int e;                 //边数
}MGraph;

void DijkstraPath(MGraph g,int *dist,int *path,int v0)   //v0表示源顶点
{
    int i,j,k;
    bool *visited=(bool *)malloc(sizeof(bool)*g.n);
    for(i=0;i<g.n;i++)     //初始化
    {
        if(g.matrix[v0][i]>0&&i!=v0)
        {
            dist[i]=g.matrix[v0][i];
            path[i]=v0;     //path记录最短路径上从v0到i的前一个顶点
        }
        else
        {
            dist[i]=INT_MAX;    //若i不与v0直接相邻,则权值置为无穷大
            path[i]=-1;
        }
        visited[i]=false;
        path[v0]=v0;
        dist[v0]=0;
    }
    visited[v0]=true;
    for(i=1;i<g.n;i++)     //循环扩展n-1次
    {
        int min=INT_MAX;
        int u;
        for(j=0;j<g.n;j++)    //寻找未被扩展的权值最小的顶点
        {
            if(visited[j]==false&&dist[j]<min)
            {
                min=dist[j];
                u=j;       
            }
        }
        visited[u]=true;
        for(k=0;k<g.n;k++)   //更新dist数组的值和路径的值
        {
            if(visited[k]==false&&g.matrix[u][k]>0&&min+g.matrix[u][k]<dist[k])
            {
                dist[k]=min+g.matrix[u][k];
                path[k]=u;
            }
        }       
    }   
}

void showPath(int *path,int v,int v0)   //打印最短路径上的各个顶点
{
    stack<int> s;
    int u=v;
    while(v!=v0)
    {
        s.push(v);
        v=path[v];
    }
    s.push(v);
    while(!s.empty())
    {
        cout<<s.top()<<" ";
        s.pop();
    }
}

int main(int argc, char *argv[])
{
    int n,e;     //表示输入的顶点数和边数
    while(cin>>n>>e&&e!=0)
    {
        int i,j;
        int s,t,w;      //表示存在一条边s->t,权值为w
        MGraph g;
        int v0;
        int *dist=(int *)malloc(sizeof(int)*n);
        int *path=(int *)malloc(sizeof(int)*n);
        for(i=0;i<N;i++)
            for(j=0;j<M;j++)
                g.matrix[i][j]=0;
        g.n=n;
        g.e=e;
        for(i=0;i<e;i++)
        {
            cin>>s>>t>>w;
            g.matrix[s][t]=w;
        }
        cin>>v0;        //输入源顶点
        DijkstraPath(g,dist,path,v0);
        for(i=0;i<n;i++)
        {
            if(i!=v0)
            {
                showPath(path,i,v0);
                cout<<dist[i]<<endl;
            }
        }
    }
    return 0;
}

测试数据:

  

  运行结果:

  

假如你有一张地图,地图上给出了每一对相邻城市的距离,从一个地点到另外一个地点,如何找到一条最短的路? 最短路算法要解决的就是这类问题。定义:给定一个有(无)向图,每一条边有一个权值 w,给定一个起始点 S 和终止点 T ,求从 S 出发走到 T 的权值最小路径,即为最短路径。最短路算法依赖一种性质:一条两顶点间的最短路径包含路径上其他最短路径。简单的说就是:最短路径的子路径是最短路径。这个用反证法很好证明。

一、松弛技术(Relaxation)

     了解最短路算法前,必须先了解松弛技术, 为什么叫松弛,有特定原因,有兴趣可以去查查相关资料,如果简化理解松弛技术,它本质上就是一个贪心操作。松弛操作:对每个顶点v∈V,都设置一个属性d[v],用来描述从源点 s 到 v 的最短路径上权值的上界,成为最短路径估计(Shortest-path Estimate),同时π[v]代表前趋。初始化伪代码:

INITIALIZE-SINGLE-SOURCE(G, s)
1  for each vertex v ∈ V[G]
2       do d[v] ← ∞
3          π[v] ← NIL
4  d[s] ← 0

   

     初始化之后,对所有 v∈V,π[v] = NIL,对v∈V – {s},有 d[s] = 0 以及 d[v] = ∞。松弛一条边(u, v),如果这条边可以对最短路径改进,则更新 d[v] 和 π[v] 。一次松弛操作可以减小最短路径估计的值 d[v] ,并更新 v 的前趋域 π[v]。下面的伪代码对边(u,v)进行了一步松弛操作:

RELAX(u, v, w)
1  if d[v] > d[u] + w(u, v)
2     then d[v] ← d[u] + w(u, v)
3          π[v] ← u

     上边的图示中,左边例子,最短路径估计值减小,右边例子,最短路径估计值不变。当发现 v 到 u 有更近的路径时,更新 d[v] 和 π[v] 。

二、Dijkstra算法

     解决最短路问题,最经典的算法是 Dijkstra算法,它是一种单源最短路算法,其核心思想是贪心算法(Greedy Algorithm),Dijkstra算法由荷兰计算机科学家Dijkstra发现,这个算法至今差不多已有50年历史,但是因为它的稳定性和通俗性,到现在依然强健。另外,Dijkstra算法要求所有边的权值非负。

     Dijkstra算法思想为:设 G = (V, E) 是一个带权有向图,把图中顶点集合 V 分成两组,第一组为已求出最短路径的顶点集合(用S 表示,初始时 S 中只有一个源点,以后每求得一条最短路径 , 就将其加入到集合 S 中,直到全部顶点都加入到 S 中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U 表示),按最短路径长度的递增次序依次把第二组的顶点加入 S 中。在加入的过程中,总保持从源点 v 到 S 中各顶点的最短路径长度不大于从源点 v 到 U 中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S 中的顶点的距离就是从 v 到此顶点的最短路径长度,U 中的顶点的距离,是从 v 到此顶点只包括 S 中的顶点为中间顶点的当前最短路径长度。伪代码:

DIJKSTRA(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  S ← Ø
3  Q ← V[G]
4  while Q ≠ Ø
5      do u ← EXTRACT-MIN(Q)
6         S ← S ∪{u}
7         for each vertex v ∈ Adj[u]
8             do RELAX(u, v, w)

      第 1 行将 d 和 π 初始化,第 2 行初始化集合 S 为空集,4 ~ 8 行每次迭代,都从 U 中选取一个点加入到 S 中,然后所有的边进行松弛操作,即每次迭代,整个图的 d 和 π 都更新一遍。过程本身很简单,下边是图示:

     源点 s 是最左端顶点。最短路径估计被标记在顶点内,阴影覆盖的边指出了前趋的值。黑色顶点在集合 S中,而白色顶点在最小优先队列 Q = V – S 中。a) 第 4 ~ 8 行 while 循环第一次迭代前的情形。阴影覆盖的顶点具有最小的 d 值,而且在第 5 行被选为顶点 u 。b) ~ f) while 循环在第一次连续迭代后的情形。每个图中阴影覆盖的顶点被选作下一次迭代第 5 行的顶点 u。f) 图中的 d 和 π 值是最终结果。

     Dijkstra算法时间主要消耗在寻找最小权值的边,和松弛所有剩余边,所以 EXTRACT-MIN(Q) 这一步,更好的方法是使用优先队列,优先队列可以用二叉堆,斐波那契堆等来实现,下面的代码,我用库自带的优先队列,经这样改造后,效率还是很可观的。

     理解最短路算法,最基础,最简单,最经典的要数这个题目:HDU 2544 最短路,纯粹的算法练习题,用Dijkstra,我写了三个代码来实现。

1)邻接矩阵 + Dijkstra,最简单的方式,当然也是最好理解的方式:

+ expand source(双击可全选代码)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*邻接矩阵 +  Dijkstra求最短路*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<cmath>
#include<algorithm>
#include<climits>
using namespace std;
 
const int NV = 102;
const int inf = INT_MAX >> 1;
 
int map[NV][NV];
bool mark[NV];
int dis[NV];
int n, m;
 
void Dijkstra( int src) {
     for ( int i = 0; i < n; i++) {
         dis[i] = map[src][i];
         mark[i] = false ;
     }
     dis[src] = 0;
     mark[src] = true ;
     for ( int i = 1; i < n; i++) {
         int minn = inf;
         int k = src;
         for ( int j = 0; j < n; j++) {
             if (!mark[j] && dis[j] < minn) {
                 k = j;
                 minn = dis[j];
             }
         }
         mark[k] = true ;
         for ( int j = 0; j < n; j++) {
             int tmp = map[k][j] + dis[k];
             if (!mark[j] && tmp < dis[j]) {
                 dis[j] = tmp;
             }
         }
     }
}
 
int main() {
     while (~ scanf ( "%d%d" , &n, &m), n || m) {
         for ( int i = 0; i < n; i++) {
             map[i][i] = inf;
             for ( int j = i + 1; j < n; j++) {
                 map[i][j] = inf;
                 map[j][i] = inf;
             }
         }
 
         while (m--) {
             int u, v, w;
             scanf ( "%d %d %d" , &u, &v, &w);
             if (map[u - 1][v - 1] > w) {
                 map[u - 1][v - 1] = w;
                 map[v - 1][u - 1] = w;
             }
         }
         Dijkstra(0);
         printf ( "%d\n" , dis[n - 1]);
     }
     return 0;
}

2)邻接表 + Dijkstra,更通用,最常见的方式:

+ expand source(双击可全选代码)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*邻接表 +  Dijkstra求最短路*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#include<climits>
using namespace std;
 
const int NV = 102;
const int NE = 20002;
int n, m;
 
struct Dijkstra {
     int n, size;
     int dis[NV], head[NV];
     int mark[NV];
 
     struct node {
         int v, dis;
         node () {}
         node ( int V, int DIS) : v(V), dis(DIS) {}
         friend bool operator < ( const node a, const node b) {
             return a.dis > b.dis;
         }
     };
 
     struct edge {
         int v, w, next;
         edge () {}
         edge ( int V, int NEXT, int W = 0) : v(V), next(NEXT), w(W) {}
     }E[NE];
 
     inline void init( int x) {
         n = x, size = 0;
         memset (head, -1, sizeof ( int ) * (x + 1));
     }
 
     inline void insert( int u, int v, int w) {
         E[size] = edge(v, head[u], w);
         head[u] = size++;
     }
 
     void print() {
         for ( int i = 0; i < n; i++) {
             printf ( "%d: " , i);
             for ( int j = head[i]; j != -1; j = E[j].next) {
                 printf ( " %d" , E[j].v);
             } puts ( "" );
         }
     }
 
     int dijkstra( int src, int des) {
         node first, next;
         priority_queue <node> Q;
         for ( int i = 0; i <= n; i++) {
             dis[i] = INT_MAX;
             mark[i] = false ;
         }
 
         dis[src] = 0;
         Q.push(node(src, 0));
 
         while (!Q.empty()) {
             first = Q.top();
             Q.pop();
             mark[first.v] = true ;
 
             for ( int i = head[first.v]; i != -1; i = E[i].next) {
                 if (mark[E[i].v]) continue ;
                 next = node(E[i].v, first.dis + E[i].w);
                 if (next.dis < dis[next.v]) {
                     dis[next.v] = next.dis;
                     Q.push(next);
                 }
             }
         } //while
         return dis[des];
     } //Dij
}G;
 
int main() {
     while (~ scanf ( "%d%d" , &n, &m), n || m) {
         G.init(n);
         while (m--) {
             int u, v, w;
             scanf ( "%d %d %d" , &u, &v, &w);
             G.insert(u - 1, v - 1, w);
             G.insert(v - 1, u - 1, w);
         }
         //G.print();
         printf ( "%d\n" , G.dijkstra(0, n - 1));
     }
     return 0;
}

3)邻接表 + 优先队列优化 + Dijkstra,效率更高,更实用的方式:

+ expand source(双击可全选代码)
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/*邻接表 +  优先队列 + Dijkstra求最短路*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<climits>
#include<queue>
using namespace std;
 
const int NV = 102;
const int NE = 20002;
int n, m;
 
struct Dijkstra {
     int n, size;
     int dis[NV], head[NV];
     int mark[NV];
 
     struct node {
         int v, dis;
         node () {}
         node ( int V, int DIS) : v(V), dis(DIS) {}
         friend bool operator < ( const node a, const node b) {
             return a.dis > b.dis;
         }
     };
 
     struct edge {
         int v, w, next;
         edge () {}
         edge ( int V, int NEXT, int W = 0) : v(V), next(NEXT), w(W) {}
     }E[NE];
 
     inline void init( int vx) {
         n = vx, size = 0;
         memset (head, -1, sizeof ( int ) * (vx + 1));
     }
 
     inline void insert( int u, int v, int w) {
         E[size] = edge(v, head[u], w);
         head[u] = size++;
     }
 
     void print() {
         for ( int i = 0; i < n; i++) {
             printf ( "%d: " , i);
             for ( int j = head[i]; j != -1; j = E[j].next) {
                 printf ( " %d" , E[j].v);
             } puts ( "" );
         }
     }
 
     int dijkstra( int src, int des) {
         node first, next;
         priority_queue <node> Q;
         for ( int i = 0; i <= n; i++) {
             dis[i] = INT_MAX;
             mark[i] = false ;
         }
 
         dis[src] = 0;
         Q.push(node(src, 0));
 
         while (!Q.empty()) {
             first = Q.top();
             Q.pop();
             mark[first.v] = true ;
 
             for ( int i = head[first.v]; i != -1; i = E[i].next) {
                 if (mark[E[i].v]) continue ;
                 next = node(E[i].v, first.dis + E[i].w);
                 if (next.dis < dis[next.v]) {
                     dis[next.v] = next.dis;
                     Q.push(next);
                 }
             }
         } //while
         return dis[des];
     } //Dij
}G;
 
int main() {
     while (~ scanf ( "%d%d" , &n, &m), n || m) {
         G.init(n);
         while (m--) {
             int u, v, w;
             scanf ( "%d %d %d" , &u, &v, &w);
             G.insert(u - 1, v - 1, w);
             G.insert(v - 1, u - 1, w);
         }
         //G.print();
         printf ( "%d\n" , G.dijkstra(0, n - 1));
     }
     return 0;
}

     如果对Dijkstra算法核心思想不是很理解,可能会问:Dijkstra算法为什么不能处理负权边?

     Dijkstra由于是贪心的,每次都找一个距源点最近的点(dmin),然后将该距离定为这个点到源点的最短路径(d[i] ← 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 使得路径减小。Dijkstra的贪心是建立在边都是正边的基础上,这样,每次往前推进,路径长度都是变大的,如果出现负边,那么先前找到的最短路就不是真正的最短路,比如上边的例子,这个算法也就算废了。

     另外,Dijkstra算法时间复杂度为O(V2 + E)。源点可达的话,O(V * lgV + E * lgV) => O(E * lgV)。当是稀疏图的情况时,此时 E = V2/ lgV,所以算法的时间复杂度可为 O(V2) 。若是斐波那契堆作优先队列的话,算法时间复杂度为O(V * lgV + E)。

三、Bellman-Ford算法

     Bellman-Ford算法能在一般情况下(存在负权边的情况)下,解决单源最短路径问题。对于给定的带权有向图 G = (V, E),其源点为 s,加权函数为 w:E → R,,对该图运行 Bellman-Ford 算法后可以返回一个布尔值,表明图中是否存在着一个从源点可达的权为负的回路。若存在这样的回路,问题无解;否则,算法产生最短路径及其权值。

     Bellman-Ford算法运用松弛技术,对每个顶点 v,逐步减小从源 s 到 v 的最短路径的权的估计值 d[v] 直至其可达到实际最短路径的权 δ(s, v) 。算法返回布尔值True,当且仅当图中不包含从源点可达的负权回路。伪代码:

BELLMAN-FORD(G, w, s)
1  INITIALIZE-SINGLE-SOURCE(G, s)
2  for i ← 1 to |V[G]| – 1
3       do for each edge (u, v) ∈ E[G]
4              do RELAX(u, v, w)
5  for each edge (u, v) ∈ E[G]
6       do if d[v] > d[u] + w(u, v)
7             then return FALSE
8  return TRUE

     第 1 行初始化每个顶点的 d 和 π 值后,算法对图中的边进行了 |V| – 1 遍操作。每一遍都是第 2 ~ 4 行for循环的一次迭代,有点类似于预处理。下边是的图示是算法对边进行四遍操作,每一遍过后的状态。在 |V – 1| 遍操作过后,第 5 ~ 8 行对负权回路进行检查,并返回适当的布尔值。图示:

     源点是顶点 s 。d 值被标记在顶点内,阴影覆盖的边指示了前趋值:如果边(u, v)被阴影覆盖,则 π[v] = u。在这个特定例子中,每一趟按照如下的顺序对边进行松弛:(t,x),(t,y),(t,z),(x,t),(y,x),(y,z),(z,x),(z,s),(s,t),(s,y)。a) 示出了对边进行第一趟操作前的情况。b) ~ e) 示出了每一趟连续对边操作后的情况。e) 中 d 和 π 值是最终结果。这个例子中,返回值是True。

     还是上边那道题目,用Bellon-Ford算法实现:(必然有最短路,所以不必判断布尔值)

     Bellman-Ford虽然很简单,但是复杂度太高,达到了O(VE),从上边图示中可以看出:(a) t,x,y,z 边的松弛是无用操作;(b) s,x,z 边的松弛是无用操作;(c) s,t,y边的松弛是无用操作;(d) s,x,y,z边的松弛是无用操作。也就是说,只有更新过的点所做的松弛才是有效操作,所以出现了更高效的算法,即SPFA:

三、SPFA(Shortest Path Faster Algorithm)

     SPFA算法是西南交通大学段凡丁于1994年发表的。它是Bellman-Ford的队列优化,时效性相对好,时间复杂度O(kE),也是单源最短路算法,同时可以处理负权边。从名字即可看出,此算法速度非同一般。

     与Bellman-ford算法类似,SPFA算法采用一系列的松弛操作以得到从某一个节点出发到达图中其它所有节点的最短路径。所不同的是,SPFA算法通过维护一个队列,使得一个节点的当前最短路径被更新之后没有必要立刻去更新其他的节点,从而大大减少了重复的操作次数。伪代码:

SPFA(G,w,s)
1. INITIALIZE-SINGLE-SOURCE(G,s)
2. INITIALIZE-QUEUE(Q)
3. ENQUEUE(Q,s)
4. While Not EMPTY(Q)
5.      Do u<-DLQUEUE(Q)
6. For 每条边(u,v) in E[G]
7.      Do tmp<-d[v]
8. Relax(u,v,w)
9. If (d[v] < tmp) and (v不在Q中)
10.     ENQUEUE(Q,v)

     Bellon-Ford算法,每次都松弛所有的边,所以造成效率低下,而SPFA的高效之处在于,它每次只松弛更新过的点连接的边,简单的过程叙述就是:

1)初始 Dis[s] = 0,其他赋值为Inf;
2)将起点s放入空队列 Q;
3)step 1. 从 Q 中选取元素u,并删除该元素;
4)step 2. 对所有和 u 相连的点 v 进行松弛,如果 v 被更新且 v 不在队列中,把 v 加进队列;
5)一直循环 step 1 和 step 2,直到队列为空。结束;

HH师兄,当年讲SPFA算法时,提到过三个问题,不妨思考一下,可以帮助更好的理解SPFA算法:

n1. 想想怎么判断负环的情况?
n2. 为什么要判断v是否在队列?
n3. 怎么样有效的判断v在不在队列之中?

答案如下:

1)如果一个节点更新了 n 次,那么存在负环;
2)如果有多个 v 在队列,第一个 v 已经把松弛做完了,剩下的 v 属于无效操作;
3)用一个数组Inqueue[],在元素进队的时候表示成 True,出队的时候标记成 False,判断的时候只要看看Inqueue[v] 是否为 True 就行了;

     另外,还需要了解的是,SPFA 的算法时间效率是不稳定的,即它对于不同的图所需要的时间有很大的差别。在最好情形下,每一个节点都只入队一次,则算法实际上变为广度优先遍历,其时间复杂度仅为O(E)。另一方面,存在这样的例子,使得每一个节点都被入队(V – 1)次,此时算法退化为 Bellman-ford算法,其时间复杂度为O(VE)。

     SPFA在负边权图上可以完全取代 Bellman-ford 算法,另外在稀疏图中也表现良好。但是在非负边权图中,为了避免最坏情况的出现,通常使用效率更加稳定的 Dijkstra 算法,以及它的使用堆优化的版本。通常的SPFA算法在一类网格图中的表现不尽如人意。

    还是上边那道题,用SPFA算法实现:

1)邻接矩阵实现,便于理解算法过程:

2)更通用的邻接表形式:

两大最强最短路算法,SPFA和Dijkstra算法的比较:

     SPFA:执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后,判断负环的话,需要记录一个节点的入队次数,超过|V|则存在负环。用SPFA的时侯,同一个节点可能会多次入队,然后多次去刷新其他节点,这样就会导致最短路条数出现重复计算(所以才能判断负环),而Dijkstra使用优先队列,虽然同一个点可以多次入队,但是mark数组保证了一个点真正pop出来刷新其他点的时候只有一次,而且必定满足最短路!

   SPFA 和 Dijkstra 同一个节点都有可能入队多次, 但是,由于Dijkstra算法不考虑负环的情况,使用优先队列,则每次pop()出来的都是最小的权值的点去刷新其他的点,因此可以保证满足最短路,同时用mark数组控制 可以保证每个节点出来刷新其他节点的机会只有一次 ,因而保证了计算最短路径的次数不会出现重复。

     而SPFA考虑到负环的情况,每个节点可以多次刷新其他节点,以此才能得到最短路并且判断是否存在负环,使用时只需要确保相同节点不要同时出现在队列中即可,由于每个节点可以多次刷新其他节点, 因此,计算最短路径数时会有重复。

     也就是,如果有负权边,则使用SPFA;如果都是正权边,Dijkstra + 优先队列效率更高,且更靠谱些。

四、Floyd算法

     前边的三种算法都是单源最短路算法,也就是用于求两点间的最短路,而Floyd是APSP(All Pairs Shortest Paths),也就是所有顶点对之间的最短路径,理解这个算法,要用到一些矩阵乘法的知识,这个我在下下篇笔记中会写,Bloyd用矩阵记录图,是一种动态规划算法,稠密图效果最佳,边权可正可负。此算法简单有效,由于三重循环结构紧凑,对于稠密图,效率要高于执行|V|次Dijkstra算法。

     Floyd算法,代码简单,可以算出任意另个结点间的距离,但是复杂度较高,达到O(n^3),所以不适合有大量数据的运算。

     Floyd算法的基本思想:从矩阵A-1开始,依次生成矩阵A0, A1,A2,……,An – 1。如果已经生成矩阵Ak – 1,那么就可以生成Ak,因为对于任意一对顶点 i 和 j ,一定满足下面两条规则中的一条:

1)如果 k 不是路径 p 的中间顶点,则 p 的所有中间顶点皆在{1,2,……,k – 1}中,其路径代价为Ak-1[i][j]。

2)如果 k 是路径 p 的中间顶点,那么该路径由从 i 到 k 的路径和从 k 到 j 的路径两部分构成,由于这两条子路径上的顶点序号都不大于k – 1,因此其路径代码分别为Ak – 1[i][k]和Ak – 1[k][j]。

     基于上述两条规则,可以得到如下求解Ak[i][j]的公式:

    Ak[i][j] = min { Ak – 1[i][j],Ak – 1[i][k] + Ak – 1[k][j] },k >= 0 和 A-1[i][j] = wij

    还是上边提到的题目,Floyd算法实现:

#include <iostream>  
#include <cstdio>  
#include <cstring>  
#define INF 0x7ffffff//如果设为0x7fffffff在执行算法时溢出。  
#define maxn 100  
using namespace std;  
int n;  
int edge[maxn][maxn];  
int a[maxn][maxn], path[maxn][maxn];  
  
void floyd() {  
    int i, j, k;  
    for( i = 0; i < n; i++ ) {  
        for( j = 0; j < n; j++) {  
            a[i][j] = edge[i][j];  
            if( i != j && a[i][j] < INF ) { path[i][j] = i; }  
            else { path[i][j] = -1; }  
        }  
    }  
    for( k = 0; k < n; k++ ) {  
        for( i = 0; i < n; i++ ) {  
            for( j = 0; j < n; j++ ) {  
                if( k == i || k == j ) { continue; }  
                if( a[i][k] + a[k][j] < a[i][j] ) {  
                    a[i][j] = a[i][k] + a[k][j];  
                    path[i][j] = path[k][j];  
                }  
            }  
        }  
   }  
}  
  
void output() {  
    int i, j;  
    int shortest[maxn];  
    for( i = 0; i < n; i++ ) {  
        for( j = 0; j < n; j++ ) {  
            if( i == j) { continue; }  
            printf( "%d->%d\t%d\t", i, j, a[i][j] );  
            memset( shortest, 0, sizeof(shortest) );  
            int k = 0;  
            shortest[k] = j;  
            while( path[i][shortest[k]] != i) {  
                k++; shortest[k] = path[i][shortest[k-1]];  
            }  
            k++; shortest[k] = i;  
            for( int t = k; t >0; t-- ) {  
                printf( "%d->", shortest[t] );  
            }  
            printf( "%d\n",shortest[0] );  
        }  
    }  
    return;  
}  
  
void init() {  
    int i, j;  
    int u, v, w;  
    scanf("%d", &n);  
    for( i = 0; i < n; i++ ) {  
        for( j = 0; j < n; j++ ) {  
            if(i != j) { edge[i][j] = INF; }  
            else { edge[i][j] = 0; }  
        }  
    }  
    while( 1 ) {  
        scanf("%d%d%d", &u, &v, &w);  
        if( u == -1 && v == -1 && w == -1 ) { break; }  
        edge[u][v] = w;  
    }  
}  
  
int main()  
{  
    init();  
    floyd();  
    output();  
    return 0;  
}  
/************************** 
测试数据: 
0 1 1 
0 3 4 
1 2 9 
1 3 2 
2 0 3 
2 1 5 
2 3 8 
3 2 6 
-1 -1 -1 
***********************/  

 

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值