最短路径
- 导读
- 一、最短路径
- 1.1 单源最短路径
- 1.2 各顶点间的最短路径
- 1.3 最短路径算法
- 二、BFS算法
- 结语
- 内容回顾
- 下一篇预告:挑战带权最短路径!
导读
大家好,很高兴又和大家见面啦!!!
欢迎继续探索图算法的精彩世界!在上一篇博客中,我们研究了最小生成树(MST)问题——它专注于为整个连通图寻找一棵连接所有顶点且总权重最小的“骨架树”,就像铺设覆盖整个城市且成本最低的电网。
今天,我们将目光转向一个同样关键但目标不同的单点决策问题:最短路径。想象你站在地图上的一个特定起点(比如“家”),目标是以最小的累计代价(时间、距离、费用)到达图中另一个点或所有其他点。这就是最短路径问题的核心!
最短路径 vs. 最小生成树:
- MST追求的是全局最优连接(覆盖所有点)。
- 最短路径追求的是点到点的最优路径(关注起点到特定终点的序列)。
本篇核心内容: 我们将
-
清晰理解最短路径的基本概念和问题分类(单源SSSP、所有顶点对APSP)。
-
简要罗列解决这些问题的经典算法(如Dijkstra用于非负权、Floyd用于所有顶点对)。
-
重点探讨第一种实战场景:如何利用 BFS(广度优先搜索)算法,高效求解非带权图(边权视为1) 中的单源最短路径问题——即从起点出发,到达图中任一点的最少边数/跳数路径。
-
深入理解 BFS用于 最短路径的核心思路 和 C语言实现细节。
你是否好奇原本用于图遍历的BFS,如何巧变身为求解最短路径的神器?它能否处理带权重的复杂道路?这些问题的答案,将在正文中一一揭晓。让我们立刻开始这段从起点寻找最优路线的旅程!
一、最短路径
要理解什么是最短路径,这里我们以一个比较形象的例子来进行理解:
在这个地图中,如果我们从家里出发要去学校的话,我们有多条路径可供选择,这里我们例举部分路径:
- 家->学校:该路径代价为5min
- 家->食堂->学校:该路径代价为6min
- 家->网吧->学校:该路径代价为20min
- 家->网吧->食堂->学校:该路径代价为16min
- ……
从这些路径中我们不难发现我们直接选择:家->学校这条路径所花费的代价是最小的,因此这条路径就是最短路径。
下面我们就来了解以下最短路径的定义:
最短路径: 指在起点和终点之间所有可能路径中,其边上权重总和最小的那条路径。路径是否最短取决于你如何定义边的权重(是距离、时间还是成本?)。
其问题本质是:在图(由点和连接点的线组成)中,以某种代价(距离、时间、费用等)为衡量标准,寻找从起点到终点的最优路线,使得累计代价最小。
1.1 单源最短路径
单源最短路径(Single-Source Shortest Path, SSSP)是图论中的核心问题,旨在解决:给定一个带权图和指定的起点(源点),计算从该起点到图中所有其他顶点的最短路径及其距离。
它不局限于单个终点,而是生成一张覆盖全图的“最短路径地图”,为后续任意终点查询提供基础。
1.2 各顶点间的最短路径
所有顶点间的最短路径(All-Pairs Shortest Paths, APSP) 是图论中的一个核心问题,其目标是计算给定带权图中每一对顶点之间的最短路径及其距离。
简单来说,就是求解图中任意起点到任意终点的最短路径问题。
1.3 最短路径算法
在最短路径问题中,主要有以下算法用于解决最短路径问题:
-
BFS(广度优先搜索)
- 适用场景:无权图(边权=1),求最少边数路径(最小跳数)。
- 时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
- 核心思想:按层次遍历图,首次到达终点的路径即最短路径。
-
Dijkstra 算法
- 适用场景:非负权重图(有向/无向)。
- 时间复杂度:
- 朴素实现: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2 + |E|) O(∣V∣2+∣E∣)
- 堆优化(主流): O ( ( ∣ V ∣ + ∣ E ∣ ) l o g ∣ V ∣ ) O((|V|+|E|)log|V|) O((∣V∣+∣E∣)log∣V∣)
- 核心思想:贪心策略 + 优先队列。每次从未确定点中选取距离起点最近的顶点,松弛其邻居。
- 关键点:
- 需用优先队列(最小堆) 加速。
- 不能处理负权边(可能导致错误结果)。
-
Bellman-Ford 算法
- 适用场景:含负权边图(无负环),可检测负权环。
- 时间复杂度: O ( ∣ V ∣ ∣ E ∣ ) O(|V||E|) O(∣V∣∣E∣)
- 核心思想: 进行 ∣ V ∣ − 1 |V|-1 ∣V∣−1 轮松弛操作(每轮遍历所有边),第 ∣ V ∣ |V| ∣V∣ 轮检测负环。
-
A 算法*
- 适用场景:非负权重图 + 有明确终点 + 启发函数(加速搜索)。
- 时间复杂度:取决于启发函数质量(最坏仍 O ( ( ∣ V ∣ + ∣ E ∣ ) l o g ∣ V ∣ ) O((|V|+|E|)log|V|) O((∣V∣+∣E∣)log∣V∣))。
- 核心思想:扩展 Dijkstra,引入启发函数 h ( v ) h(v) h(v)(估算 v v v 到终点的代价),优先扩展 f ( v ) = g ( v ) + h ( v ) f(v)=g(v)+h(v) f(v)=g(v)+h(v) 最小的点( g ( v ) g(v) g(v) 为起点到 v v v 的实际代价)。
- 要求: h ( v ) h(v) h(v) 必须为可采纳启发函数(不能高估真实代价)。
- 应用:游戏寻路、机器人导航(如 h ( v ) h(v) h(v) 用欧式距离)。
-
Floyd-Warshall 算法
- 适用场景:所有顶点对最短路径(APSP),支持负权边(无负环)。
- 时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(∣V∣3)
- 核心思想: 动态规划。定义 d i s t [ i ] [ j ] dist[i][j] dist[i][j] 为通过顶点 1.. k 1..k 1..k 中转时 i → j i→j i→j 的最短距离,迭代更新 k k k。
- 核心公式: d i s t [ i ] [ j ] = m i n ( d i s t [ i ] [ j ] , d i s t [ i ] [ k ] + d i s t [ k ] [ j ] ) dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]) dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j])
-
Johnson 算法(高效处理带负权的 APSP)
- 适用场景:含负权边的稀疏图(无负环)的 APSP 问题。
- 时间复杂度: O ( ∣ V ∣ ∣ E ∣ l o g ∣ V ∣ ) O(|V||E| log |V|) O(∣V∣∣E∣log∣V∣) (优于 Floyd 在稀疏图的表现)
- 步骤:
- 添加虚拟节点 s s s,连接所有顶点(边权=0)。
- 用 Bellman-Ford 计算 s s s 到各点的最短距离 h ( v ) h(v) h(v)(势能函数)。
- 重加权边: w ^ ( u , v ) = w ( u , v ) + h ( u ) − h ( v ) ŵ(u,v) = w(u,v) + h(u) - h(v) w^(u,v)=w(u,v)+h(u)−h(v)(新权重非负)。
- 对每个顶点运行 Dijkstra(基于 w ^ ŵ w^)。
- 还原真实距离: d i s t ( u , v ) = d i s t ^ ( u , v ) − h ( u ) + h ( v ) dist(u,v) = dist̂(u,v) - h(u) + h(v) dist(u,v)=dist^(u,v)−h(u)+h(v)
各算法对应的应用场景如下所示:
场景需求 | 推荐算法 |
---|---|
无权图(最少边数) | BFS |
非负权图(单源最短路径) | Dijkstra |
含负权边(单源,需检测负环) | Bellman-Ford |
非负权图 + 终点明确 + 需加速 | A* |
小规模图或稠密图(所有点对最短路径) | Floyd-Warshall |
含负权的稀疏图(所有点对) | Johnson |
在现阶段,我们主要需要了解3种算法:
- 单源路径算法(SSSP):
- BFS算法
- Dijkstra算法
- 各顶点间的最短路径(APSP):
- Floyd算法
二、BFS算法
BFS算法用于解决非带权图的单源最短路径问题。所谓的非带权图,我们可以视作各边权值为1的特殊带权图。
在之前我们有介绍过广度优先生成树,整个生成树的创建过程实际上就是由近到远的图遍历过程:
- 生成树的根结点就是单源最短路径中的起点(源点)
- 从根结点到各结点的路径就是单源最短路径中起点到各结点的最短路径
因此整个算法的实现我们可以参考广度优先生成树的算法实现:
- 通过
visited[]
数组来记录当前顶点是否被访问 - 通过
d[]
数组来记录当前顶点到源点的最短路径 - 通过
path[]
数组来记录当前顶点的前驱顶点 - 通过队列
queue[]
来实现整个遍历过程 - 算法的核心逻辑为
BFS
- 对未访问顶点的处理:
- 通过
d[]
来记录当前顶点到原点的路径长度 - 通过
path[]
来记录当前顶点的前驱顶点 - 通过
visited[]
来更改当前顶点的访问状态
- 通过
对于BFS
算法的核心逻辑,我们在广度优先遍历的篇章中已经详细介绍,这里就不再展开,感兴趣的朋友可以点击链接阅读相关内容。
下面我们直接来看C语言代码:
int visited[MAXVERSIZE]; // 遍历标记数组
int d[MAXVERSIZE]; // 路径长度数组
int path[MAXVERSIZE]; // 路径前驱数组
void BFS_MIN_Distance(graph* g, int u) {for (int i = 0; i < MAXVERSIZE; i++) {visited[i] = false; // 初始化所有顶点为未访问状态d[i] = -1; // 初始化顶点与源点不存在路径path[i] = -1; // 初始化所有顶点没有前驱顶点}visited[u] = true; // 访问源点d[u] = 0; // 源点距离源点路径长度为0path[u] = -1; // 源点不存在前驱顶点Queue* Q = CreatQueue(); // 遍历队列EnQueue(Q, u); // 源点入队while (!IsEmpty(Q)) {int p = DeQueue(Q); // 队首元素出队for (int w = FirstNeighbor(g, p); w >= 0; w = NextNeighbor(g, p, w)) {if (!visited[w]) {visited[w] = true; // 访问当前顶点path[w] = p; // 当前顶点的前驱顶点为pd[w] = d[p] + 1; // 当前顶点距离源点的距离为顶点p距离源点的距离+1EnQueue(Q, w); // 当前顶点入队}}}DestroyQueue(&Q); // 销毁队列
}
BFS
求解单源最短路径问题的思路很简单,只需要在广度优先生成树的基础上额外维护两个数组用于记录顶点距离源点的最短路径和其前驱顶点即可。
由于解决的是非带权图的单源最短路径问题,因此我们只需要对图完成最基本的BFS
即可,整个算法的时间复杂度为: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
上述算法是在连通图中求解单源最短路径问题,如果是在非连通图中,我们可以通过遍历visited[]
来找到未被访问的顶点。这个问题在广度优先遍历的章节中同样有过介绍,这里我就不再赘述。
结语
内容回顾
通过本篇博客,我们一起深入探讨了最短路径问题的核心概念与应用场景。主要内容总结如下:
-
最短路径问题本质
- 在带权图中寻找起点到终点之间累计权重最小的路径(如最短时间、最短距离或最小成本路径),是解决实际路线规划问题的理论基础。
-
问题分类与算法全景
- 单源最短路径(SSSP):从指定起点到所有其他顶点的最短路径(如导航起点到全城地点)。
- 所有顶点间的最短路径(APSP):计算图中任意两点间的最短路径(如城市交通网全局分析)。
-
简要介绍了六类主流算法(
BFS
、Dijkstra
、Bellman-Ford
、A*
、Floyd
、Johnson
)及其适用场景 。 -
BFS算法的实战应用
- 重点解析了如何利用广度优先搜索(
BFS
)高效解决无权图的最短路径问题:- 将无权图视为边权均为1的特殊带权图,通过
d[]
数组记录距离、path[]数组回溯路径 - 基于层次遍历特性,首次访问的路径即为最短路径,时间复杂度仅 O ( V + E ) O(V + E ) O(V+E)
- 提供完整C语言实现代码,适用于网络路由、社交关系等最小跳数应用场景
- 将无权图视为边权均为1的特殊带权图,通过
- 重点解析了如何利用广度优先搜索(
下一篇预告:挑战带权最短路径!
至此,我们掌握了无权图的最短路径解决方案。但当图中路径的代价不再均等(如公路有长短、任务有耗时)时,BFS
就无法满足需求了!
下一篇博客将聚焦两类核心算法:
🔹 Dijkstra算法
- 解决带非负权重图的单源最短路径问题,用贪心策略+优先队列实现高效搜索。你将学到:
- 如何通过 “松弛操作” 动态更新最短路径
- 堆优化实现细节与时间复杂度分析
- 为何它无法处理负权边
🔹 Floyd算法
- 一网打尽所有顶点间的最短路径(APSP),动态规划的经典应用:
- 三重循环背后的子问题分解思想
- 中转顶点k的迭代优化逻辑
- 负权环的检测方法
代码实战预告:下一篇将提供两类算法的C语言实现,手把手带你写出高效的最短路径计算器!
觉得这篇博客有帮助? ❤️ 您的支持是我持续创作的动力!
👉 关注我,第一时间获取技术干货更新
👍 点赞收藏,方便日后复习查找
💬 评论提问或分享你的见解,我们一起探讨
🔄 转发给需要的朋友,共同进步!
下一站,我们将在加权图的复杂道路中寻找最优解——Dijkstra
与Floyd
算法深度解析,不见不散!