Dijkstra算法、Bellman-Ford算法、Floyd算法

Dijkstra算法

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

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

其基本思想是,设置顶点集合S并不断地作贪心选择来扩充这个集合。一个顶点属于集合S当且仅当从源到该顶点的最短路径长度已知。

初始时,S中仅含有源。设uG的某一个顶点,把从源到u且中间只经过S中顶点的路称为从源到u的特殊路径,并用数组dist记录当前每个顶点所对应的最短特殊路径长度。Dijkstra算法每次从V-S中取出具有最短特殊路长度的顶点u,将u添加到S中,同时对数组dist作必要的修改。一旦S包含了所有V中顶点,dist就记录了从源到所有其它顶点之间的最短路径长度。

例如,对下图中的有向图,应用Dijkstra算法计算从源顶点1到其它顶点间最短路径的过程列在下表中。

 《Dijkstra算法、Bellman-Ford算法、Floyd算法》

Dijkstra算法的迭代过程:

《Dijkstra算法、Bellman-Ford算法、Floyd算法》

主题好好理解上图!

以下是具体的实现(C/C++):

/***************************************
* About:    有向图的Dijkstra算法实现
***************************************/
 
#include <iostream>
using namespace std;
 
const int maxnum = 100;
const int maxint = 999999;
 
 
void Dijkstra(int n, int v, int *dist, int *prev, int c[maxnum][maxnum])
{
    bool s[maxnum];    // 判断是否已存入该点到S集合中
    for(int i=1; i<=n; ++i)
    {
        dist[i] = c[v][i];
        s[i] = 0;     // 初始都未用过该点
        if(dist[i] == maxint)
            prev[i] = 0;
        else
            prev[i] = v;
    }
    dist[v] = 0;
    s[v] = 1;
 
    // 依次将未放入S集合的结点中,取dist[]最小值的结点,放入结合S中
    // 一旦S包含了所有V中顶点,dist就记录了从源点到所有其他顶点之间的最短路径长度
    for(int i=2; i<=n; ++i)
    {
        int tmp = maxint;
        int u = v;
        // 找出当前未使用的点j的dist[j]最小值
        for(int j=1; j<=n; ++j)
            if((!s[j]) && dist[j]<tmp)
            {
                u = j;              // u保存当前邻接点中距离最小的点的号码
                tmp = dist[j];
            }
        s[u] = 1;    // 表示u点已存入S集合中
 
        // 更新dist
        for(int j=1; j<=n; ++j)
            if((!s[j]) && c[u][j]<maxint)
            {
                int newdist = dist[u] + c[u][j];
                if(newdist < dist[j])
                {
                    dist[j] = newdist;
                    prev[j] = u;
                }
            }
    }
}
 
void searchPath(int *prev,int v, int u)
{
    int que[maxnum];
    int tot = 1;
    que[tot] = u;
    tot++;
    int tmp = prev[u];
    while(tmp != v)
    {
        que[tot] = tmp;
        tot++;
        tmp = prev[tmp];
    }
    que[tot] = v;
    for(int i=tot; i>=1; --i)
        if(i != 1)
            cout << que[i] << " -> ";
        else
            cout << que[i] << endl;
}
 
int main()
{
    freopen("input.txt", "r", stdin);
    // 各数组都从下标1开始
    int dist[maxnum];     // 表示当前点到源点的最短路径长度
    int prev[maxnum];     // 记录当前点的前一个结点
    int c[maxnum][maxnum];   // 记录图的两点间路径长度
    int n, line;             // 图的结点数和路径数
 
    // 输入结点数
    cin >> n;
    // 输入路径数
    cin >> line;
    int p, q, len;          // 输入p, q两点及其路径长度
 
    // 初始化c[][]为maxint
    for(int i=1; i<=n; ++i)
        for(int j=1; j<=n; ++j)
            c[i][j] = maxint;
 
    for(int i=1; i<=line; ++i)  
    {
        cin >> p >> q >> len;
        if(len < c[p][q])       // 有重边
        {
            c[p][q] = len;      // p指向q
            c[q][p] = len;      // q指向p,这样表示无向图
        }
    }
 
    for(int i=1; i<=n; ++i)
        dist[i] = maxint;
    for(int i=1; i<=n; ++i)
    {
        for(int j=1; j<=n; ++j)
            printf("%8d", c[i][j]);
        printf("\n");
    }
 
    Dijkstra(n, 1, dist, prev, c);
 
    // 最短路径长度
    cout << "源点到最后一个顶点的最短路径长度: " << dist[n] << endl;
 
    // 路径
    cout << "源点到最后一个顶点的路径为: ";
    searchPath(prev, 1, n);
}


Bellman-Ford算法

 Bellman-ford算法是求含负权图的单源最短路径算法,效率很低,但代码很容易写。即进行不停地松弛(原文是这么写的,为什么要叫松弛,争议很大),每次松弛把每条边都更新一下,若n-1次松弛后还能更新,则说明图中有负环,无法得出结果,否则就成功完成。Bellman-ford算法有一个小优化:每次松弛先设一个旗帜flag,初值为FALSE,若有边更新则赋值为TRUE,最终如果还是FALSE则直接成功退出。Bellman-ford算法浪费了许多时间做无必要的松弛,所以SPFA算法用队列进行了优化,效果十分显著,高效难以想象。SPFA还有SLF,LLL,滚动数组等优化。
<Bellman-Ford算法>
  Dijkstra算法中不允许边的权是负权,如果遇到负权,则可以采用Bellman-Ford算法。
  Bellman-Ford算法能在更普遍的情况下(存在负权边)解决单源点最短路径问题。对于给定的带权(有向或无向)图 G=(V,E),其源点为s,加权函数 w是 边集 E 的映射。对图G运行Bellman-Ford算法的结果是一个布尔值,表明图中是否存在着一个从源点s可达的负权回路。若不存在这样的回路,算法将给出从源点s到 图G的任意顶点v的最短路径d[v]。
  适用条件&范围
  1.单源最短路径(从源点s到其它所有顶点v);
  2.有向图&无向图(无向图可以看作(u,v),(v,u)同属于边集E的有向图);
  3.边权可正可负(如有负权回路输出错误提示);
  4.差分约束系统;
  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 次。
  每实施一次松弛操作,最短路径树上就会有一层顶点达到其最短距离,此后这层顶点的最短距离值就会一直保持不变,不再受后续松弛操作的影响。(但是,每次还要判断松弛,这里浪费了大量的时间,怎么优化?单纯的优化是否可行?)
  如果没有负权回路,由于最短路径树的高度最多只能是|v|-1,所以最多经过|v|-1遍松弛操作后,所有从s可达的顶点必将求出最短距离。如果 d[v]仍保持 +∞,则表明从s到v不可达。
  如果有负权回路,那么第 |v|-1 遍松弛操作仍然会成功,这时,负权回路上的顶点不会收敛。
引申:SPFA算法
  算法简介
  SPFA(Shortest Path Faster Algorithm)是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。
算法流程
  算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。直到队列为空时算法结束。

不过改的spfa算法,注意每个节点进入队列的次数至多为n-1次(一共n个节点),若进入大于等于n次了,则说明图中存在负权回路,此时正好满足题目中时光倒流的要求,另外注意,vector每次用的时候清空。
下面是spfa算法的简单说明:
我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是松弛:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
注意对刚出队列的节点的所有相邻节点都要做松弛操作,不管该节点是否在队列中,不在队列中的松弛点加入到队列即可。

#include <iostream>
#include <fstream>
#include <vector>
#include <queue>
#define INF 999999999
using namespace std;

struct E
{
   int v;
   int w;
}edges[6000];

vector<E> v[1008];
bool visited[1008];
int dist[1008];
int enterCount[1008];

int n,m,w,edgeCount;

void addEdge(int e,int s,int t)
{
   edgeCount++;
   edges[edgeCount].v=s;
   edges[edgeCount].w=t;
   v[e].push_back(edges[edgeCount]);
}

bool spfa()
{
   int i;
   queue<int> q;
   visited[1]=1;
   dist[1]=0;
   enterCount[1]=1;
   for(i=2;i<=n;i++)
  {
      visited[i]=0;
      dist[i]=INF;
      enterCount[i]=0;
   }
    q.push(1); 
    while(!q.empty())
   {
      int temp=q.front();
      q.pop();
      visited[temp]=0;
      for(i=0;i<v[temp].size();i++)
     {
        if(dist[v[temp][i].v]>dist[temp]+v[temp][i].w)
        {
            dist[v[temp][i].v]=dist[temp]+v[temp][i].w;
         if(!visited[v[temp][i].v]) 
       { 
           visited[v[temp][i].v]=1;
           enterCount[v[temp][i].v]++;
           if(enterCount[v[temp][i].v]>=n)
          return true;
         q.push(v[temp][i].v);
       }
     }
  }
}
   return false;
}

int main()
{
   //ifstream cin("1.txt");
   int f,i;
   cin>>f;
   while(f--)
  {
     cin>>n>>m>>w;
     edgeCount=0;
     for(i=1;i<=n;i++)
     v[i].clear();
     for(i=0;i<m;i++)
    {
        int s,e,t;
        cin>>s>>e>>t;
        addEdge(s,e,t);
        addEdge(e,s,t);
    }
     for(i=0;i<w;i++)
    {
        int s,e,t;
        cin>>s>>e>>t;
        addEdge(s,e,-t);
     }

     if(spfa())
     cout<<"YES"<<endl;
     else
     cout<<"NO"<<endl;
  }
  return 0;
}


#include<iostream>
using namespace std;
const int fMax = 505;
const int eMax = 5205;
const int wMax = 99999;



struct{
int sta, end, time;
}edge[eMax];
int point_num, edge_num, dict[fMax];



bool bellman_ford()
{
   int i, j;
   for(i = 2; i <= point_num; i ++)
   dict[i] = wMax;
   for(i = 1; i < point_num; i ++)
  {
     bool finish = true; // 加个全部完成松弛的判断,优化了50多MS。
     for(j = 1; j <= edge_num; j ++)
    {
      int u = edge[j].sta;
      int v = edge[j].end;
      int w = edge[j].time;
      if(dict[v] > dict[u] + w)
      { // 松弛。
       dict[v] = dict[u] + w;
       finish = false;
      }
     }
   if(finish) break;
  }
  for(i = 1; i <= edge_num; i ++)
 { // 是否存在负环的判断。
    int u = edge[i].sta;
    int v = edge[i].end;
    int w = edge[i].time;
    if(dict[v] > dict[u] + w)
     return false;
  }
  return true;
}



int main()
{
   int farm;
   scanf("%d", &farm);
   while(farm --)
  {
     int field, path, hole;
     scanf("%d %d %d", &field, &path, &hole);
     int s, e, t, i, k = 0;
     for(i = 1; i <= path; i ++)
    {
       scanf("%d %d %d", &s, &e, &t); // 用scanf代替了cin,优化了100多MS。
       k ++;
       edge[k].sta = s;
       edge[k].end = e;
       edge[k].time = t;
       k ++;
       edge[k].sta = e;
       edge[k].end = s;
       edge[k].time = t;
     }
   for(i = 1; i <= hole; i ++)
  {
     scanf("%d %d %d", &s, &e, &t);
     k ++;
     edge[k].sta = s;
     edge[k].end = e;
     edge[k].time = -t;
   }
   point_num = field;
   edge_num = k;
   if(!bellman_ford()) 
   printf("YES\n");
   else printf("NO\n");
  }
   return 0;
} 


Floyd算法

这个算法主要要弄懂三个循环的顺序关系。

弗洛伊德(Floyd)算法过程:
1、用D[v][w]记录每一对顶点的最短距离。
2、依次扫描每一个点,并以其为基点再遍历所有每一对顶点D[][]的值,看看是否可用过该基点让这对顶点间的距离更小。

算法理解:

最短距离有三种情况:
1、两点的直达距离最短。(如下图<v,x>)
2、两点间只通过一个中间点而距离最短。(图<v,u>)
3、两点间用通过两各以上的顶点而距离最短。(图<v,w>)

对于第一种情况:在初始化的时候就已经找出来了且以后也不会更改到。
对于第二种情况:弗洛伊德算法的基本操作就是对于每一对顶点,遍历所有其它顶点,看看可否通过这一个顶点让这对顶点距离更短,也就是遍历了图中所有的三角形(算法中对同一个三角形扫描了九次,原则上只用扫描三次即可,但要加入判断,效率更低)。
对于第三种情况:如下图的五边形,可先找一点(比如x,使<v,u>=2),就变成了四边形问题,再找一点(比如y,使<u,w>=2),可变成三角形问题了(v,u,w),也就变成第二种情况了,由此对于n边形也可以一步步转化成四边形三角形问题。(这里面不用担心哪个点要先找哪个点要后找,因为找了任一个点都可以使其变成(n-1)边形的问题)。

《Dijkstra算法、Bellman-Ford算法、Floyd算法》

结合代码 并参照上图所示 我们来模拟执行下 这样才能加深理解:
第一关键步骤:当k执行到x,i=v,j=u时,计算出v到u的最短路径要通过x,此时v、u联通了。
第二关键步骤:当k执行到u,i=v,j=y,此时计算出v到y的最短路径的最短路径为v到u,再到y(此时v到u的最短路径上一步我们已经计算过来,直接利用上步结果)。
第三关键步骤:当k执行到y时,i=v,j=w,此时计算出最短路径为v到y(此时v到y的最短路径长在第二步我们已经计算出来了),再从y到w。

依次扫描每一点(k),并以该点作为中介点,计算出通过k点的其他任意两点(i,j)的最短距离,这就是floyd算法的精髓!同时也解释了为什么k点这个中介点要放在最外层循环的原因.

对于这个算法,网上有一个证明的版本:

 floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在),floyd算法加入了这个概念  Ak(i,j):表示从i到j中途不经过索引比k大的点的最短路径

    这个限制的重要之处在于,它将最短路径的概念做了限制,使得该限制有机会满足迭代关系,这个迭代关系就在于研究:假设Ak(i,j)已知,是否可以借此推导出Ak-1(i,j)。

    假设我现在要得到Ak(i,j),而此时Ak(i,j)已知,那么我可以分两种情况来看待问题:1. Ak(i,j)沿途经过点k;2. Ak(i,j)不经过点k。如果经过点k,那么很显然,Ak(i,j) = Ak-1(i,k) + Ak-1(k,j),为什么是Ak-1呢?因为对(i,k)和(k,j),由于k本身就是源点(或者说终点),加上我们求的是Ak(i,j),所以满足不经过比k大的点的条件限制,且已经不会经过点k,故得出了Ak-1这个值。那么遇到第二种情况,Ak(i,j)不经过点k时,由于没有经过点k,所以根据概念,可以得出Ak(i,j)=Ak-1(i,j)。现在,我们确信有且只有这两种情况—不是经过点k,就是不经过点k,没有第三种情况了,条件很完整,那么是选择哪一个呢?很简单,求的是最短路径,当然是哪个最短,求取哪个,故得出式子:

    Ak(i,j) = min( Ak-1(i,j), Ak-1(i,k) + Ak-1(k,j) )

    现在已经得出了Ak(i,j) = Ak-1(i,k) + Ak-1(k,j)这个递归式,但显然该递归还没有一个出口,也就是说,必须定义一个初始状态,事实上,这个初始状态取决于索引k是从0开始还是从1开始,上面的代码是C写的,是以0为开始索引,但一般描述算法似乎习惯用1做开始索引,如果是以1为开始索引,那么初始状态值应设置为A0了,A0(i,j)的含义不难理解,即从i到j的边的距离。也就是说,A0(i,j) = cost(i,j) 。由于存在i到j不存在边的情况,也就是说,在这种情况下,cost(i,j)无限大,故A0(i,j) = oo(当i到j无边时)

    到这里,已经列出了求取Ak(i,j)的整个算法了,但是,最终的目标是求dist(i,j),即i到j的最短路径,如何把Ak(i,j)转换为dist(i,j)?这个其实很简单,当k=n(n表示索引的个数)的时候,即是说,An(i,j)=dist(i,j)。那是因为当k已经最大时,已经不存在索引比k大的点了,那这时候的An(i,j)其实就已经是i到j的最短路径了。

    从floyd算法中不难看出,要设计一个好的动态规划算法,首先需要研究是否能把目标进行重新诠释(这一步是最关键最富创造力的一步),转化为一个可以被分解的子目标,如果可以转化,就要想办法寻找数学等式使目标收敛为子目标,如果这一步可以实现了,还需要研究该递归收敛式的出口,即初始状态是否明确(这一步往往已经简单了)。

如果需要保存最短路径,需要借助path数组:

其中我们用 path 数组记录 经过路径 其实 path 的定义如下  path[i][j]  = k 表示 是最短路径 i-……j  和 j 的直接 前驱  为 k 即是: i–>……………–>k ->j

举例子:

如  1-> 5->4   4->3->6  此时 path[1][6] = 0 ; 0表示 1->6 不通  当我们 引入 节点 k = 4 此时有 1->5->4->3->6 显然有 paht[1][6] = 3 = paht[4][6] = paht[k][6]

于是有 path[i][j] = path[k][j] 

对于 1->5 相邻边 我们可以在初始化时候 有 paht[1][5] = 1;

如是对于 最短路径 1->5->4->3->6 有 paht[1][6] = 3; paht[1][3]= 4; paht[1][4] = 5; paht[1][5] =1 如此逆推不难得到 最短路径记录值 。

[cpp] 
view plain
 copy
 
《Dijkstra算法、Bellman-Ford算法、Floyd算法》
《Dijkstra算法、Bellman-Ford算法、Floyd算法》

  1. #include “iostream”  
  2. #include “vector”  
  3. #include “stack”  
  4. #include “fstream”  
  5. using namespace std;  
  6. std::vector<vector<int> > weight;  
  7. std::vector<vector<int> > path;  
  8. int vertexnum;  
  9. int edgenum;  
  10. const int intmax = 10000;  
  11. void initialvector(){  
  12.     weight.resize(vertexnum);//路径权重数组  
  13.     path.resize(vertexnum);//保存最短路径数组,记录前继  
  14.     for(int i = 0;i < vertexnum;i++){//建立数组  
  15.         weight[i].resize(vertexnum,intmax);  
  16.         path[i].resize(vertexnum,-1);  
  17.     }  
  18. }  
  19. void getData(){//获取数据  
  20.     ifstream in(“data”);  
  21.     in>>vertexnum>>edgenum;  
  22.     initialvector();  
  23.     int from,to;  
  24.     double w;  
  25.     while(in>>from>>to>>w){  
  26.         weight[from][to] = w;  
  27.         path[from][to] = from;//to的前继是from  
  28.         weight[from][from] = 0;//自身到自身的权重为0  
  29.         path[from][from] = from;  
  30.         weight[to][to] = 0;  
  31.         path[to][to] = to;  
  32.     }  
  33. }  
  34. void floyd(){  
  35.     for(int k = 0;k < vertexnum;k++)  
  36.         for(int i= 0;i < vertexnum;i++)  
  37.             for(int j = 0;j < vertexnum;j++){  
  38.                 if((weight[i][k] > 0 && weight[k][j] && weight[i][k] < intmax && weight[k][j] < intmax) && (weight[i][k] + weight[k][j] < weight[i][j])){//前面一部分是防止加法溢出  
  39.                     weight[i][j] = weight[i][k] + weight[k][j];  
  40.                     path[i][j] = path[k][j];  
  41.                 }  
  42.             }  
  43. }  
  44. void displaypath(int source,int dest){  
  45.     stack<int> shortpath;  
  46.     int temp = dest;  
  47.     while(temp != source){  
  48.         shortpath.push(temp);  
  49.         temp = path[source][temp];  
  50.     }  
  51.     shortpath.push(source);  
  52.     cout<<“short distance:”<<weight[source][dest]<<endl<<“path:”;  
  53.     while(!shortpath.empty()){  
  54.         cout<<shortpath.top()<<” “;  
  55.         shortpath.pop();  
  56.     }  
  57. }  
  58. int main(int argc, char const *argv[])  
  59. {  
  60.     getData();    
  61.     for(int i = 0;i < vertexnum;i++){    
  62.         for(int j = 0;j < vertexnum;j++){    
  63.             cout<<weight[i][j]<<“\t”;    
  64.         }    
  65.         cout<<endl;    
  66.     }  
  67.     floyd();  
  68.     displaypath(2,1);  
  69.     return 0;  
  70. }  

数据:

6 9
0 1 3
0 3 4
0 5 5
1 2 1
1 5 5
2 3 5
3 1 3
4 3 3
4 5 2
5 3 2



    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/hhu1506010220/article/details/52261986
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞