【数据结构与算法】第40篇:图论(四):最短路径——Dijkstra算法与Floyd算法

一、最短路径问题概述

1.1 问题定义

给定带权图,找出两个顶点之间权值之和最小的路径。

分类

  • 单源最短路径:一个起点到其他所有点的最短路径

  • 多源最短路径:任意两点之间的最短路径

1.2 应用场景

场景 说明
导航系统 从A地到B地的最短路线
网络路由 数据包传输的最优路径
社交网络 两个人之间的最短关系链
游戏开发 AI寻路

二、Dijkstra算法

2.1 算法思想

Dijkstra算法是贪心算法 ,从起点开始,每次选择距离起点最近且未处理的顶点,然后松弛它的邻接边。

限制 :不能处理负权边(因为贪心假设已找到最短路径不再更新)。

步骤

  1. 初始化:dist[start]=0,其他dist=∞

  2. 选择未处理中dist最小的顶点u

  3. 标记u为已处理

  4. 对u的每个邻接点v,若dist[u]+w(u,v) < dist[v],更新dist[v]

  5. 重复2-4,直到所有顶点被处理

2.2 图解示例

text

复制代码
图结构:
    1
  0 — 1 (4)
  |   / \
(2) (1) (5)
  | /     \
  2 — 3 — 4
    (3) (2)

从0开始:
初始:dist[0]=0, dist[1]=∞, dist[2]=∞, dist[3]=∞, dist[4]=∞

选0:松弛邻接点
  dist[1]=2, dist[2]=1
  visited: {0}

选2(dist=1):松弛
  dist[3]=1+3=4
  visited: {0,2}

选1(dist=2):松弛
  dist[3]=min(4, 2+5)=4, dist[4]=2+?(无直接边)
  visited: {0,2,1}

选3(dist=4):松弛
  dist[4]=4+2=6
  visited: {0,2,1,3}

选4(dist=6):无松弛
完成

2.3 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define INF INT_MAX

// Dijkstra算法(邻接矩阵)
void dijkstra(int graph[MAX_VERTICES][MAX_VERTICES], int n, int start) {
    int dist[MAX_VERTICES];
    int visited[MAX_VERTICES] = {0};
    int prev[MAX_VERTICES];  // 记录前驱节点,用于还原路径
    
    // 初始化
    for (int i = 0; i < n; i++) {
        dist[i] = INF;
        prev[i] = -1;
    }
    dist[start] = 0;
    
    for (int count = 0; count < n - 1; count++) {
        // 找到未处理中距离最小的顶点
        int u = -1;
        for (int i = 0; i < n; i++) {
            if (!visited[i] && (u == -1 || dist[i] < dist[u])) {
                u = i;
            }
        }
        
        if (dist[u] == INF) break;  // 剩余顶点不可达
        
        visited[u] = 1;
        
        // 松弛邻接边
        for (int v = 0; v < n; v++) {
            if (graph[u][v] != INF && !visited[v] && 
                dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
                prev[v] = u;
            }
        }
    }
    
    // 输出结果
    printf("起点 %d 到各点的最短距离:\n", start);
    for (int i = 0; i < n; i++) {
        if (dist[i] == INF) {
            printf("  %d: 不可达\n", i);
        } else {
            printf("  %d: %d", i, dist[i]);
            // 打印路径(可选)
            if (i != start) {
                printf(" (路径: %d", i);
                int p = prev[i];
                while (p != -1) {
                    printf(" <- %d", p);
                    p = prev[p];
                }
                printf(")");
            }
            printf("\n");
        }
    }
}

int main() {
    int n = 5;
    int graph[MAX_VERTICES][MAX_VERTICES];
    
    // 初始化无穷大
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            graph[i][j] = (i == j) ? 0 : INF;
        }
    }
    
    // 添加边
    graph[0][1] = graph[1][0] = 2;
    graph[0][2] = graph[2][0] = 1;
    graph[1][2] = graph[2][1] = 1;
    graph[1][3] = graph[3][1] = 5;
    graph[2][3] = graph[3][2] = 3;
    graph[3][4] = graph[4][3] = 2;
    
    dijkstra(graph, n, 0);
    
    return 0;
}

运行结果:

text

复制代码
起点 0 到各点的最短距离:
  0: 0
  1: 2 (路径: 1 <- 0)
  2: 1 (路径: 2 <- 0)
  3: 4 (路径: 3 <- 2 <- 0)
  4: 6 (路径: 4 <- 3 <- 2 <- 0)

2.4 堆优化版(适合稀疏图)

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 1000
#define INF INT_MAX

// 优先队列节点
typedef struct {
    int vertex;
    int dist;
} Node;

// 简单堆实现(这里用数组模拟,实际可用二叉堆)
// 实际工程中建议用二叉堆或优先队列

void dijkstraHeap(int graph[MAX_VERTICES][MAX_VERTICES], int n, int start) {
    int dist[MAX_VERTICES];
    int visited[MAX_VERTICES] = {0};
    
    for (int i = 0; i < n; i++) dist[i] = INF;
    dist[start] = 0;
    
    for (int count = 0; count < n; count++) {
        int u = -1;
        for (int i = 0; i < n; i++) {
            if (!visited[i] && (u == -1 || dist[i] < dist[u])) {
                u = i;
            }
        }
        if (dist[u] == INF) break;
        visited[u] = 1;
        
        for (int v = 0; v < n; v++) {
            if (graph[u][v] != INF && dist[u] + graph[u][v] < dist[v]) {
                dist[v] = dist[u] + graph[u][v];
            }
        }
    }
    
    // 输出...
}

三、Floyd算法

3.1 算法思想

Floyd算法是动态规划思想:逐步允许经过更多顶点作为中间点,更新最短路径。

核心公式dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

步骤

  1. 初始化dist[i][j] = 直接边的权值(无直接边则为∞,i=j为0)

  2. 对每个顶点k作为中间点,尝试更新所有i,j

  3. 最终dist[i][j]即为最短路径长度

3.2 动态规划推导

状态定义dp[k][i][j] 表示允许经过前k个顶点时,i到j的最短路径

状态转移dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

空间优化:可以只用二维数组,k循环在外层。

3.3 代码实现

c

复制代码
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

#define MAX_VERTICES 100
#define INF INT_MAX

// Floyd算法
void floyd(int graph[MAX_VERTICES][MAX_VERTICES], int n) {
    int dist[MAX_VERTICES][MAX_VERTICES];
    int next[MAX_VERTICES][MAX_VERTICES];  // 记录路径
    
    // 初始化
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            dist[i][j] = graph[i][j];
            if (graph[i][j] != INF && i != j) {
                next[i][j] = j;
            } else {
                next[i][j] = -1;
            }
        }
    }
    
    // Floyd核心:三重循环
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dist[i][k] != INF && dist[k][j] != INF &&
                    dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                    next[i][j] = next[i][k];
                }
            }
        }
    }
    
    // 输出结果
    printf("任意两点之间的最短距离:\n");
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            if (i == j) {
                printf("  %d到%d: 0\n", i, j);
            } else if (dist[i][j] == INF) {
                printf("  %d到%d: 不可达\n", i, j);
            } else {
                printf("  %d到%d: %d", i, j, dist[i][j]);
                // 打印路径
                if (next[i][j] != -1) {
                    printf(" (路径: %d", i);
                    int p = next[i][j];
                    while (p != j) {
                        printf(" -> %d", p);
                        p = next[p][j];
                    }
                    printf(" -> %d)", j);
                }
                printf("\n");
            }
        }
        printf("\n");
    }
}

int main() {
    int n = 4;
    int graph[MAX_VERTICES][MAX_VERTICES];
    
    // 初始化
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            graph[i][j] = (i == j) ? 0 : INF;
        }
    }
    
    // 添加边
    graph[0][1] = 5;
    graph[0][3] = 10;
    graph[1][2] = 3;
    graph[2][3] = 1;
    
    floyd(graph, n);
    
    return 0;
}

运行结果:

text

复制代码
任意两点之间的最短距离:
  0到0: 0
  0到1: 5 (路径: 0 -> 1)
  0到2: 8 (路径: 0 -> 1 -> 2)
  0到3: 9 (路径: 0 -> 1 -> 2 -> 3)

  1到0: 不可达
  1到1: 0
  1到2: 3 (路径: 1 -> 2)
  1到3: 4 (路径: 1 -> 2 -> 3)

  2到0: 不可达
  2到1: 不可达
  2到2: 0
  2到3: 1 (路径: 2 -> 3)

  3到0: 不可达
  3到1: 不可达
  3到2: 不可达
  3到3: 0

四、Dijkstra vs Floyd

对比项 Dijkstra Floyd
解决问题 单源最短路径 多源最短路径
核心思想 贪心 动态规划
时间复杂度 O(V²) / O(E log V) O(V³)
空间复杂度 O(V) O(V²)
负权边 不能处理 可以处理(不能有负环)
负环检测 不能 可以(检测dist[i][i] < 0)
代码复杂度 中等 简单(三重循环)
适用场景 单起点,无负权 顶点少,需全部距离

五、负权边问题

5.1 Dijkstra为什么不能处理负权

Dijkstra的贪心假设:已选中的顶点最短路径不会再被更新。负权边可能使已选中的顶点路径变短,破坏贪心性质。

text

复制代码
示例:
0 → 1 (1)
0 → 2 (3)
1 → 2 (-2)

Dijkstra从0开始:
选1(dist=1),标记1为已处理
但实际0→2的最短路径是0→1→2(1+(-2)=-1)
此时2已经被标记为dist=3,无法更新

5.2 Floyd处理负权边

Floyd可以处理负权边,但不能有负环(绕一圈总权值为负)。有负环时最短路径为-∞。

c

复制代码
// 检测负环
for (int i = 0; i < n; i++) {
    if (dist[i][i] < 0) {
        printf("图中存在负环\n");
        break;
    }
}

六、路径还原

两种算法都可以记录路径:

Dijkstra :用prev[]数组,每次更新时记录前驱。

Floyd :用next[][]数组,next[i][j]表示i到j路径中i的下一个顶点。

c

复制代码
// 打印路径函数
void printPath(int next[MAX_VERTICES][MAX_VERTICES], int i, int j) {
    if (next[i][j] == -1) {
        printf("无路径");
        return;
    }
    printf("%d", i);
    while (i != j) {
        i = next[i][j];
        printf(" -> %d", i);
    }
}

七、算法选择建议

场景 推荐 理由
单源、无负权、稠密图 Dijkstra(邻接矩阵) O(V²)简单实现
单源、无负权、稀疏图 Dijkstra(堆优化) O(E log V)高效
单源、有负权(无负环) Bellman-Ford 可处理负权
多源、顶点少(V≤500) Floyd 代码简单,O(V³)可接受
多源、顶点多 多次Dijkstra 每点跑一次
需要检测负环 Floyd / Bellman-Ford 可检测

八、小结

这一篇我们学习了最短路径的两种经典算法:

算法 核心 时间复杂度 适用场景
Dijkstra 贪心 + 松弛 O(V²) / O(E log V) 单源、无负权
Floyd 动态规划 O(V³) 多源、顶点少

关键代码模板

c

复制代码
// Dijkstra核心
while (还有未处理顶点) {
    选dist最小的u;
    visited[u] = 1;
    for (v : u的邻接点) {
        if (dist[u] + w < dist[v]) dist[v] = dist[u] + w;
    }
}

// Floyd核心
for (int k = 0; k < n; k++)
    for (int i = 0; i < n; i++)
        for (int j = 0; j < n; j++)
            if (dist[i][k] + dist[k][j] < dist[i][j])
                dist[i][j] = dist[i][k] + dist[k][j];

下一篇我们讲拓扑排序与关键路径。


九、思考题

  1. Dijkstra算法中,为什么不能处理负权边?能举出一个反例吗?

  2. Floyd算法的三重循环中,为什么k必须在最外层?如果把k放在内层会怎样?

  3. 如何用Dijkstra算法找出从起点到终点的具体路径(不只是距离)?

  4. 如果图中存在负环,Floyd算法的结果会怎样?如何检测?

欢迎在评论区讨论你的答案。

相关推荐
SccTsAxR2 小时前
算法进阶:贪心策略证明全攻略与二进制倍增思想深度解析
c++·经验分享·笔记·算法
2301_792674862 小时前
java学习day27(算法)
java·学习·算法
啦啦啦!2 小时前
c++AI大模型接入SDK项目
开发语言·数据结构·c++·人工智能·算法
lcj25112 小时前
【C语言】自定义类型1:结构体
c语言·开发语言·算法
jaysee-sjc2 小时前
十七、Java 高级技术入门全解:JUnit、反射、注解、动态代理
java·开发语言·算法·junit·intellij-idea
yongui478342 小时前
MATLAB模糊控制的粒子群算法(Fuzzy-PSO)实现
数据结构·算法·matlab
sali-tec2 小时前
C# 基于OpenCv的视觉工作流-章49-人脸检测
图像处理·人工智能·opencv·算法·计算机视觉
不爱吃炸鸡柳2 小时前
4道经典算法题代码详解:从两数之和到链表两两交换
算法·链表·哈希算法
cmpxr_3 小时前
【C】隐式类型转换
c语言·c++·算法