The Shortest Path of Graphs | 图的最短路问题

Albert Wang / 2023-01-07 / 800 Words/has been Read   Times


问题背景 #

假设我们有下面这张图,图中边的权重表示两点之间的距离。我们希望找到一条最短的路径从这张图的 A 点出发,最终走到 E 点,该怎样考虑这个问题呢?

image-20230107153434693

这个问题很具有现实的意义,假设两点表示两个城市的距离,我们往往希望能够走最短距离的那条路到达目的地。而且这个问题也有一个隐含的条件,那就路的距离不可能为负数,所以我们可以考虑使用 Dijkstra 算法来进行求解。

Dijkstra 算法 #

Dijkstra 算法考虑对每一个点进行标号,每个点对应着两种状态(固定标号和临时标号,表示已经确定最短路径和尚未确定),一开始只有出发点的状态是已经确定最短路径的,然后会在迭代的时候去更新标号点,同时也会对每个点的最短路径进行更新。

图表模拟 #

下面我们先用图表的方式来进行模拟 Dijkstra 的求解过程。首先我们用一个数组来存储每个点的状态,最开始只有 A 点的状态是确定的,A 点到 A 点的距离初始化为 0,然后初始化 A 点到其余各点的距离,如果 A 点到某一个点没有路径,则初始化为无穷大。从 A 点开始迭代,更新 A 点到到各点的最短距离,如下图所示

image-20230107155215789

这时我们能确定 A 点到 C 点的最短距离是 3,然后就将 C 点的状态设置为确定,再从 C 点开始迭代,继续更新到其余未确定点之间的最小距离,如下图

image-20230107155554263

这时我们确定了,A 点到 B 点的最短距离是 5,然后将 B 点设置为确定,再更新其他未确定的点。直到最终目的地的点被标注为已确定,如下图所示

image-20230107155915271

这样我们就得到了 A 点到 E 点的最短路径 18. 下面我们用代码来实现上面的算法。

代码实现 #

我们采用邻接矩阵来实现 dijkstra 算法

#include <stdlib.h>
#include <stddef.h>
#include <stdio.h>
#include <string.h>

#define INF 10000001
#define N 101

int graph[N][N];

void dijkstra(int *distance, int *flag, int n)
{
    int a, b;
    for (int i = 0; i < n - 1; i++) {
        a = -1;
        b = INF;
        for (int j = 1; j < n; j++) { // 找出发点到 j 点的最小距离
            if (flag[j] == 0 && distance[j] < b) {
                a = j;
                b = distance[j];
            }
        }
        if (a == -1) {  // 没有路到终点
            break;
        }
        if (a == n - 1) { // 已经找到了到终点的最短路
            break;
        }

        flag[a] = 1; // 改为固定标号
        for (int j = 1; j < n; j++) {
            if (flag[j] == 0 && (b + graph[a][j] < distance[j])) {
                distance[j] = b + graph[a][j];
            }
        }
    }

    if (distance[n - 1] >= INF) {
        printf("Impossible!\n");
    } else {
        printf("%d\n", distance[n - 1]);
    }
}

int main()
{
    int a, b, c, n, m;
    int distance[N] = {0}; // 起点到该点的最短距离
    int flag[N] = {0}; // 固定标号与临时标号

    scanf("%d%d", &n, &m); // n 个点, m 条边

    // 初始化各个数据结构
    for (int i = 0; i < N; i++) {
        for (int j = 0; j < N; j++) {
            if (i == j) {
                graph[i][j] = 0;
            } else {
                graph[i][j] = INF;
            }
        }
    }

    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &c);
        a--;
        b--;  // 假设输入的图节点是从下标 1 开始
        graph[a][b] = c;
        // graph[b][a] = c; // 如果是无向图
    }

    for (int i = 1; i < n; i++) {
        distance[i] = graph[0][i];
    }

    flag[0] = 1; // 初始只有起点是固定标号

    dijkstra(distance, flag, n); 
}

最开始的图可以按照下面的方式输入,然后就能得到最短路径 18.

5 6
1 2 10
1 3 3
1 4 20
2 4 5
3 5 15
4 5 11

image-20230107163306062

Bellman-Ford 算法 #

大家可能注意到了在上面的代码中我们是按照节点进行遍历的,Bellman-Ford 算法则是按照边来进行遍历的。之所以能这么做是因为下面的条件,

  • 如果最短路存在,则最短路经过的边数不超过 n - 1 条;
  • 当负权存在时,最短路都不一定存在(有负环时一定不存在)。

然后我们的核心代码可以这样进行修改,将原来的邻接矩阵换成边的数组,然后我们就可以根据上面的条件控制遍历次数在 n - 1 次。distance[i] 依然表示从出发点到 i 点的最短距离。

if (distance[edge[i].v] > distance[edge[i].u] + edge[i].w) {
    distance[edge[i].v] = distance[edge[i].u] + edge[i].w;
}

上面这段代码表示如果直接从出发点到 v 点的距离大于先从出发点到 u 点,再从 u 点到 v 点的距离,就对 distance 数组进行更新

#include <stdio.h>
#include <stdbool.h>

#define INF 10000001
#define N 101

typedef struct Edge_ {
    int u;
    int v;
    int w;
} Edge;

void bellmanFord(int *distance, Edge *edge, int n, int m)
{
    bool flag;
    for (int k = 0; k < n - 1; k++) {
        flag = false;
        for (int i = 0; i < m; i++) {
            if (distance[edge[i].v] > distance[edge[i].u] + edge[i].w) {
                distance[edge[i].v] = distance[edge[i].u] + edge[i].w;
            }
        }
        if (!flag) {
            break;
        }
    }

    // 检测负权回路
    flag = false;
    for (int i = 0; i < m; i++) {
        if (distance[edge[i].v] > distance[edge[i].u] + edge[i].w) {
            flag = true;
            break;
        }
    }
    if (flag) {
        printf("该图有负权回路\n");
    } else {
        printf("%d\n", distance[n - 1]);
    }
}

int main()
{
    int a, b, c, n, m;
    int distance[N] = {0}; // 起点到该点的最短距离
    Edge edge[N] = {0};

    scanf("%d%d", &n, &m); // n 个点, m 条边

    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &c);
        a--; b--;
        edge[i].u = a;
        edge[i].v = b;
        edge[i].w = c;
    }

    for (int i = 1; i < n; i++) {
        distance[i] = INF;
    }
    bellmanFord(distance, edge, n, m);
}

用 Bellman-Ford 算法我们同样可以得到最短路径 18.

image-20230107170556175

Floyd 算法 #

如果我们要求任意两点之间的距离,用 Dijkstra 算法可能需要调用 n 次,这时用 Floyd 算法就可以很好地解决这个问题。Floyd 算法利用了动态规划的思想,它的状态转移方程可以用下面的方式表达。 $$ d[i][j] = min(d[i][j], d[i][k] + d[k][j]), 其中 0 <= i, j, k < n $$ 其核心思想也就是说如果直接从 i 点到 j 点的距离小于先从 i 点到 k 点,再从 k 点到 j 点,那我们就要选择距离最小的点。

Floyd 算法的代码实现如下,它的代码实现相对与前面两个算法来说更为简单一些,也更容易记忆,建议优先掌握 Floyd 算法。

#include <stdio.h>
#include <stdbool.h>

#define INF 10000001
#define N 101

int graph[N][N];

void floyd(int n)
{
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (graph[i][j] > graph[i][k] + graph[k][j]) {
                    graph[i][j] = graph[i][k] + graph[k][j];
                }
            }
        }
    }
   
    printf("%d\n", graph[0][n - 1]);
}

int main()
{
    int a, b, c, n, m;
    
    scanf("%d%d", &n, &m); // n 个点, m 条边
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (i == j) {
                graph[i][j] = 0;
            } else {
                graph[i][j] = INF;
            }
        }
    }
    for (int i = 0; i < m; i++) {
        scanf("%d%d%d", &a, &b, &c);
        a--; b--;
        graph[a][b] = c;
    }
    floyd(n);
}

Floyd 算法的测试结果如下:

image-20230107173230871

Last modified on 2023-01-07