图-最短路径-Dijkstra及其变种

目录

  • 最短路径

    • Dijkstra 算法

      • 算法思想
      • 具体实现
      • 最短路径
      • Dijkstra+DFS

最短路径

最短路径问题:

给定任意的图G(V,E) 和起点 S,终点 T,如何求从 S 到 T 的最短路径。

解决最短路径的常用方法有

  • Dijkstra 算法
  • Bellman-Ford 算法
  • SPFA 算法
  • Floyd 算法

这里主要对 Dijkstra 算法及其变种进行总结。

Dijkstra 算法

算法思想

Dijkstra 算法用来解决单源最短路径问题,即给定图 G 和起点 s,通过算法得到 S 到达其他每个顶点的最短距离。

Dijkstra 算法的基本思想

对于图 G(V,E)设置集合 S,存放已被访问的顶点,然后每次从集合 V-S 中选择与起点 s 的最短距离最小的一个顶点(记为u),访问并加入集合 S。之后,令顶点 u 为中介点,优化起点 s 与所有从 u 能到达的顶点的最短距离。这样的操作执行 n (n为顶点个数),直到集合 S 已经包含所有顶点。

详细策略:

首先设置集合 S 存放已经被访问的顶点,然后执行 n 次(顶点数)下面的两个步骤

  1. 每次从集合 V - S 中选择与起点 s 最短距离的一个顶点(记为 u),访问并且加入集合 S
  2. 令顶点 u 为中介点,优化所有能从 u 到达的顶点 v 之间的最短距离

具体实现

在实现过程中,有两个主要问题需要考虑:

  • 集合 S 的实现
  • 起点 s 到达顶点 Vi ( 0<= i <= n-1)的最短距离的实现
  1. 集合 S可以用一个 bool 型数组 vis[] 来实现,即当 vis[i] == true 时表示顶点 Vi 已经被访问
  2. 令 int 型数组 d[] 表示起点到达顶点 Vi 的最短距离,初始时除了起点 s 的 d[s] = 0 以外,其余顶点都赋为一个很大的数

伪代码如下:

void Dijkstra(G, d[], s) {
    初始化;
    for(循环n次) {
       u = 使 d[u] 最小的,还未访问的顶点标号;
       记录 q 被访问;
       for(从u出发能够到达的所有顶点v){
         if(v未被访问 && 以u为中介点使得s到v的最短距离d[v]更优) {
           优化d[v];
         }
       }
    }
}

邻接矩阵版

const int MAXV = 1000;  // 最大顶点数
const int INF = 1e9;        // INF很大的数字

int n, G[MAXV][MAXV];       // n 为顶点数,MAXV为最大顶点数
int d[MAXV];                        // 起点到达各点的最短路径长度
bool vis[MAXV] = {false};   // 是否被访问过

// s 为起点
void Dijkstra(int s) {
        fill(d, d+MAXV, INF);   // 初始化距离为最大值
    d[s] = 0;
    for(int i = 0; i < n; i++) {    // 重复 n 次
      int u = -1, MIN = INF;

      // 遍历找到未访问顶点中 d[] 最小的
      for(int j = 0; j < n; j++) {
        if(vis[j] == false && d[j] < MIN) {
          u = j;
          MIN = d[j];
        }
      }

      if(u == -1) return;   // 找不到小于 INF 的 d[u],说明剩下的顶点和起点 s 不连通
      vis[u] = true;    // 标记 u 为已访问
      for(int v = 0; v < n; v++) {
        // 如果v未访问 && u 能到达 v && 以 u 为中介点可以使 d[v] 更优
        if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) {
          d[v] = d[u] + G[u][v];
        }
      }
    }
}

时间复杂度:外层循环 O(V),内层循环 O(V),枚举 v 需要 O(V) ,总复杂度为 O(V*(V+V)) = O(V2 )。

邻接表版

struct Node {
  int v, dis;   // v 为目标顶点,dis 为边权
};
vector<Node> Adj[MAXV]; //图G,Adj[u]存放从顶点 v 出发可以到达的所有顶点
int n;  // n为顶点数
int d[MAXV];    // 起点到达各点的最短路径长度
bool vis[MAXV] = {false};

void Dijkstra(int s) {// s 为起点
  fill(d, d+MAXV, INF);
  d[s] = 0; // 到自身为 0
  for(int i = 0; i < n; i++) {  // 循环 n 次
    int u = -1, MIN = INF;

    // 遍历找到未访问顶点中 d[] 最小的
      for(int j = 0; j < n; j++) {
        if(vis[j] == false && d[j] < MIN) {
          u = j;
          MIN = d[j];
        }
      }

      if(u == -1) return;   // 找不到小于 INF 的 d[u],说明剩下的顶点和起点 s 不连通
      vis[u] = true;    // 标记 u 为已访问

    // 和邻接矩阵不同
    for(int j=0; j < Adj[u][j].size(); j++) {
      int v = Adj[u][j].v;  // 获得u能直接到达的顶点
      // v 未访问 && 以 u 为中介点到达 v 比 d[v] 更短
      if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]) {
        // 更新
        d[v] = d[u] + Adj[u][j].dis;
      }
    }
  }
}

当题目给的是无向边时(双向边)而不是有向边时,只需要把无向边当成两条指向相反的有向边即可。

最短路径

我们这时候还没说到最短路径如何记录,我们回到伪代码,有这样一步

    for(从u出发能够到达的所有顶点v){
         if(v未被访问 && 以u为中介点使得s到v的最短距离d[v]更优) {
           优化d[v];
         }
       }

我们在这个时候吧这个信息记录下来,也就是设置一个 pre[] 数组,令 pre[v] 表示从起点 s 到顶点 v 的最短路径上的前一个顶点(前驱结点)的编号,这样,当伪代码中条件成立时,就把 u 赋给 pre[v] ,最终就记录下来了。

伪代码变成了:

for(从u出发能够到达的所有顶点v){
         if(v未被访问 && 以u为中介点使得s到v的最短距离d[v]更优) {
           优化d[v];
           令 v 的前驱为 u
         }
       }

以邻接矩阵为例:

const int MAXV = 1000;  // 最大顶点数
const int INF = 1e9;        // INF很大的数字

int n, G[MAXV][MAXV];       // n 为顶点数,MAXV为最大顶点数
int d[MAXV];                        // 起点到达各点的最短路径长度
int pre[MAXV];  // 记录最短路径
bool vis[MAXV] = {false};   // 是否被访问过

// s 为起点
void Dijkstra(int s) {
        fill(d, d+MAXV, INF);   // 初始化距离为最大值
    d[s] = 0;
    for(int i = 0; i < n; i++) {    // 重复 n 次
      int u = -1, MIN = INF;

      // 遍历找到未访问顶点中 d[] 最小的
      for(int j = 0; j < n; j++) {
        if(vis[j] == false && d[j] < MIN) {
          u = j;
          MIN = d[j];
        }
      }

      if(u == -1) return;   // 找不到小于 INF 的 d[u],说明剩下的顶点和起点 s 不连通
      vis[u] = true;    // 标记 u 为已访问
      for(int v = 0; v < n; v++) {
        // 如果v未访问 && u 能到达 v && 以 u 为中介点可以使 d[v] 更优
        if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]) {
          d[v] = d[u] + G[u][v];
          pre[v] = u; // 记录 v 的前驱结点是 u
        }
      }
    }
}

// 输出结点
void DFS(int s, int v) {
  if(v == s) {  // 已经递归到起点 s
    printf("%d\n", s);
    return;
  }
  DFS(s, pre[v]);           // 递归访问 v 的前驱顶点 pre[v]
  printf("%d\n", v);    // 从最深处 return 回来之后输出每一层的结点号
}

多条最短路径

我们此时已经学会了 Dijkstra 和最短路径的求法,但是通常情况下最短路径不止一条。

于是碰到这种有两条以上可以达到的最短路径,题目就会给出第二标尺(第一标尺是距离),要求在所有最短路径中选择第二标尺最优的一条路径。

通常有以下三种方式:

  1. 给每条边再增加一个边权(比如花费)
  2. 给每个点增加一个点权
  3. 直接问有多少条最短路径

对于这三种提问,都只需要增加一个数组来存放新增的边权或点权或最短路径数,然后修改优化 d[v] 的那个步骤即可。

对于以上三种提问,分别的解决办法:

  1. 新增边权。以新增边权代表花费为例,用 cost[u][v] 代表从 u->v 的花费,增加一个数组 c[] ,令从起点 s 到顶点 u 的最少花费为 c[u],初始化时只有 c[s] = 0,其余都为 INF(距离最大)。然后在 d[u] + G[u][v] < d[v] 时,更新 d[v]c[v],而当 d[u] + G[u][v] == d[v]c[u]+cost[u][v] < c[v]时更新 c[v]
for(int v = 0; v < n; v++) {
        // 如果v未访问 && u 能到达 v && 以 u 为中介点可以使 d[v] 更优
        if(vis[v] == false && G[u][v] != INF) {
          if(d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            c[v] = c[u] + cost[u][v];
          }else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) {
            c[v] = c[u] + cost[u][v];
          }
        }
      }
  1. 同上,就是换成权重数组。
  2. 只需要增加一个数组 num[] ,令从起点 s 到达顶点 u 的最短路径条数为 num[u] ,初始化时,num[s] = 1,其余为 0。当 d[u] + G[u][v] < d[v] 时,让 num[v] 继承 num[u]。而当 d[u] + G[u][v] == d[v] ,将 num[u] 加到 num[v] 上。代码如下:
for(int v = 0; v < n; v++) {
        // 如果v未访问 && u 能到达 v && 以 u 为中介点可以使 d[v] 更优
        if(vis[v] == false && G[u][v] != INF) {
          if(d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            num[v] = num[u];
          }else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]) {
            num[v] += num[u]; //最短距离相同时累加 num
          }
        }
      }

通过两个题目来巩固最短路径:

PTA 1003,这个题目非常值得一做,考虑 Dijkstra 算法三种变种,能够很好的熟悉 Dijkstra。

Dijkstra+DFS

上述题目一般都用了 Dijkstra 来做,然后有多个标尺的情况下很容易出错,这里介绍一种更通用、模板化的方法——Dijkstra+DFS。

在算法中 pre 数组总是保持着最优路径,而这显然需要在执行 Dijkstra 算法的过程中来确定何时更新每个节点 v 的前驱结点 pre[v] 。更简单的方法是:先在Dijkstra算法中记录下所有最短路径(只考虑距离),然后从这些路径中选择一条第二标尺最优的路径

  1. 使用 Dijkstra 算法记录所有最短路径

由于需要记录所有最短路径,所以每个节点就会存在多个前驱结点,这样可以使用 vector<int> pre[MAXV] 来保存前驱结点。对于每个节点 v 来说,pre[v] 就是一个变长数组 vector ,里面用来存放结点 v 的所有能产生最短路径的前驱结点。

接下来考虑更新 d[v] 的过程中 pre 数组的变化。首先,如果 d[u]+G[u][v] < d[v],说明以 u 为中介点可以使 d[v] 更优,此时令 v 的前驱结点为 u,并且即使之前 pre[v] 中已经存放了若干结点,此处也应该清空,然后再添加 u,因为此时之前保存的不是最优路径了。

if(d[u] + G[u][v] < d[v]) {
  d[v] = d[u] + G[u][v];
  pre[v].clear();
  pre[v].push(u);
}else if(d[u] + G[u][v] == d[v]) {
  pre[v].push(u);
}

那么我们就可以编写完整的代码如下:

vector<int> pre[MAXV];
// s 为起点
void Dijkstra(int s) {
        fill(d, d+MAXV, INF);   // 初始化距离为最大值
    d[s] = 0;
    for(int i = 0; i < n; i++) {    // 重复 n 次
      int u = -1, MIN = INF;

      // 遍历找到未访问顶点中 d[] 最小的
      for(int j = 0; j < n; j++) {
        if(vis[j] == false && d[j] < MIN) {
          u = j;
          MIN = d[j];
        }
      }

      if(u == -1) return;   // 找不到小于 INF 的 d[u],说明剩下的顶点和起点 s 不连通
      vis[u] = true;    // 标记 u 为已访问
      for(int v = 0; v < n; v++) {
        // 如果v未访问 && u 能到达 v && 以 u 为中介点可以使 d[v] 更优
        if(vis[v] == false && G[u][v] != INF) {
          if(d[u] + G[u][v] < d[v]) {
            d[v] = d[u] + G[u][v];
            pre[v].clear();
            pre[v].push_back(u);
          }else if(d[u] + G[u][v] == d[v]) {
            pre[v].push_back(u);
          }
        }
      }
    }
}
  1. 遍历所有最短路径,找出一条第二标尺最优的路径。

由于每个结点的前驱结点可能有多个,遍历的过程就会形成一递归树,我们可以使用DFS来寻找到最优路径。对树进行遍历时,每次到达叶子结点时就会产生一条完整的最短路径,每次得到一条路径,就可以计算第二标尺的值,令其和当前第二标尺的最优值进行比较,如果比最优值更优,则更新最优值,并用这条路径覆盖当前的最优路径。

我们考虑一下这个递归函数该如何实现。

  • 作为全局变量的第二标尺最优值 optValue
  • 记录最优路径的数组 path(使用 vector 来存储)
  • 临时记录 DFS 遍历到叶子结点时的路径 tempPath(使用vector存储)

然后考虑递归函数的两大构成:递归边界和递归式,如果访问的结点是叶子结点(起点st),那么说明到达了递归边界,此时 tempPath 存放了一条路径,求出第二标尺的值和optValue比较。

在递归过程中生成 tempPath。只要在访问当前结点 v 时将 v 加到 tempPath 的最后面,然后遍历 pre[v] 进行递归,等 pre[v] 的所有结点遍历完毕后再把 tempPath 最后面的 v 弹出。

int optValue;
vector<int> pre[MAXV];
vector<int> tempPath, path;
int st; // 出发结点

void DFS(int v) {
  if(st == v) {
    // 递归到了出发结点
    tempPath.push(v);
    int value;
    计算路径 tempPath 上的value值
    if(value优于optValue) {
      optValue = value;
      path = tempPath;
    }
    tempPath.pop_back();    // 把刚刚加入的结点弹出来哦
    return;
  }else {
    tempPath.push_back(v);  // 把当前访问结点加入临时路径 tempPath 的最后面
    for(int i = 0; i < pre[v].size(); i++) {
      DFS(pre[v][i]);
    }
    tempPath.pop_back();
  }
}

当我们遇到的是点权或者边权的时候,我们只需要修改计算value值的过程。

但是需要注意的是,存放在 tempPath 中路径的结点是逆序的,因此访问结点需要倒着进行。

// 边权之和
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--) {
  int id = tempPath[i], idNext = tempPath[i-1];
  value += V[id][nextId];
}

// 点权之和
int value = 0;
for(int i = tempPath.size() - 1; i > 0; i--) {
  int id = tempPath[i];
  value += W[id];
}

如果需要记录最短路径的条数,也可以在 DFS 的过程中,每到达一次叶子结点令该全局变量加 1。

PTA 1030 ,这个题使用了 Dijkstra + DFS 的思想,可以好好学习借鉴一下。

可以和之前的 PTA 1003 结合起来看,基本上 Dijkstra 就没啥毛病了。

但是 DIjkstra 的缺点就是遇到负权图的时候就很无力了,所以这个时候出现了新的算法。

原文地址:https://www.cnblogs.com/veeupup/p/12543659.html

时间: 2024-11-02 20:44:51

图-最短路径-Dijkstra及其变种的相关文章

hdu1818 It&#39;s not a Bug, It&#39;s a Feature!(隐式图最短路径Dijkstra)

题目链接:点击打开链接 题目描述:补丁在修bug时,有时也会引入新的bug,假设有n(n<=20)个潜在的bug和m(m<=100)个补丁,每个补丁用两个长度为n的字符串表示,其中字符串的每个位置表示一个bug.第一个串表示打补丁之前的状态('-'表示在该位置不存在bug,'+'表示该位置必须存在bug,0表示无所谓),第二个串表示打补丁之后的状态('-'表示不存在,'+'表示存在,0表示不变).每个补丁都有一个执行时间,你的任务是用最少的时间把一个所有bug都存在的软件通过打补丁的方式变得没

java数据结构和算法------图(最短路径Dijkstra)

1 package iYou.neugle.graph; 2 3 import java.util.ArrayList; 4 import java.util.List; 5 6 //创建图过程的代码在图的那篇博文中,此处直接使用 7 public class Dijkstra { 8 private MyGraph1 graph; 9 private int start; 10 private int maxNum; 11 private int[] distance;// 起始点到终点距离

最短路径Dijkstra算法和Floyd算法整理、

转载自:http://www.cnblogs.com/biyeymyhjob/archive/2012/07/31/2615833.html 最短路径—Dijkstra算法和Floyd算法 Dijkstra算法 1.定义概览 Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止.Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹

数据结构:单源最短路径--Dijkstra算法

Dijkstra算法 单源最短路径 给定一带权图,图中每条边的权值是非负的,代表着两顶点之间的距离.指定图中的一顶点为源点,找出源点到其它顶点的最短路径和其长度的问题,即是单源最短路径问题. Dijkstra算法 求解单源最短路径问题的常用方法是Dijkstra(迪杰斯特拉)算法.该算法使用的是贪心策略:每次都找出剩余顶点中与源点距离最近的一个顶点. 算法思想 带权图G=<V,E>,令S为已确定了最短路径顶点的集合,则可用V-S表示剩余未确定最短路径顶点的集合.假设V0是源点,则初始 S={V

最短路径--Dijkstra(狄克斯特拉)算法

最短路径 路径的概念:       在一个无权的图中,若从一顶点到另一顶点存在着一条路径,则称该路径长度为该路径上所经过的边的数目,它等于该路径上的顶点数减1.       由于从一顶点到另一顶点可能存在着多条路径,每条路径上所经过的边数可能不同,即路径长度不同,我们把路径长度最短(即经过的边数最少)的那条路径叫做最短路径,其路径长度叫做最短路径长度或最短距离.       对于带权的图,考虑路径上各边上的权值,则通常把一条路径上所经边的权值之和定义为该路径的路径长度或称带权路径长度.     

有向网络(带权的有向图)的最短路径Dijkstra算法

什么是最短路径? 单源最短路径(所谓单源最短路径就是只指定一个顶点,最短路径是指其他顶点和这个顶点之间的路径的权值的最小值) 什么是最短路径问题? 给定一带权图,图中每条边的权值是非负的,代表着两顶点之间的距离.指定图中的一顶点为源点,找出源点到其它顶点的最短路径和其长度的问题,即是单源最短路径问题. 什么是Dijkstra算法? 求解单源最短路径问题的常用方法是Dijkstra(迪杰斯特拉)算法.该算法使用的是贪心策略:每次都找出剩余顶点中与源点距离最近的一个顶点. 算法思想 带权图G=<V,

最短路径-Dijkstra算法与Floyd算法

一.最短路径 ①在非网图中,最短路径是指两顶点之间经历的边数最少的路径. AE:1    ADE:2   ADCE:3   ABCE:3 ②在网图中,最短路径是指两顶点之间经历的边上权值之和最短的路径. AE:100   ADE:90   ADCE:60   ABCE:70 ③单源点最短路径问题 问题描述:给定带权有向图G=(V, E)和源点v∈V,求从v到G中其余各顶点的最短路径. 应用实例--计算机网络传输的问题:怎样找到一种最经济的方式,从一台计算机向网上所有其它计算机发送一条消息. ④每

贪心算法初探3——最短路径(Dijkstra算法)

问题描述:给定有向带权图G=(V,E),其中每条边的权是非负实数.此外,给定V中的一个顶点,称为源点.现在要计算从源点到所有其他各顶点的最短路径长度,这里路径长度指路上各边的权之和. 算法设计:这个问题一般采用迪杰斯特拉算法(Dijkstra)算法思想是先求出长度最短的一条路径,再参照该最短路径求出长度次短的一条路径,直到求出从源点到其他各个顶点的最短路径. 算法基本思想:先假定源点为u,顶点集合V被划分为两部分:集合V以及集合V-S.初始时S中仅含有u,其中S的顶点到源点的最短路径已经确定.集

最短路径---Dijkstra迪杰特斯拉算法---《数据结构》严蔚敏

// exam1.cpp : 定义控制台应用程序的入口点. // #include "stdafx.h" #include <iostream> #include <stack> using namespace std; #define MAXVEX 20 #define INT_MAX 10000 typedef struct ArcNode { int adj; void* info; }ArcNode; typedef ArcNode AdjMat[MAX