Lec9 Graph Algorithms(3)
Lec9 Graph Algorithms(3)
图论的世界,探索!!!😎解决了图的遍历之后,接下来该你了:最短路径问题!🤔
前情提要:路径成本与路径长度
- 路径成本 (Path Cost):
- 定义:路径成本是路径上所有边的成本之和。对于每条边,可能有不同的权重或成本,路径成本是这些边的成本总和。
- 应用:路径成本通常用于加权图中,特别是在寻找最短路径或最低成本路径时。
- 路径长度 (Path Length):
- 定义:路径长度是路径中边的数量,即从起始节点到目标节点经过的边的数量。
- 应用:路径长度是一个无权重的度量,通常用于不考虑边的权重或成本的情境中,或者当我们只关心路径的边数而非成本时。
看图会更加直观的理解这两个概念:
最短路径问题概述
问题定义:
- 给定一个图 $ G = (V, E) $ 和一个“源”顶点 $ s V $,找到从源顶点 $ s $ 到每个顶点 $ V $ 的最小成本路径。
- 单源最短路径问题:即给定一个源点,求出源点到所有其他顶点的最短路径。
问题的多种变体:
- 无权图 vs. 加权图:在无权图中,所有的边具有相同的权重,而在加权图中,每条边都有不同的权重。
- 有环图 vs. 无环图:有些最短路径算法要求图是无环的,而有些算法则适用于有环图。
- 只有正权重 vs. 有正有负权重:有些算法仅适用于图中所有边的权重都是正数,而有些算法则可以处理边权为负的图。
为什么研究最短路径问题?
- 旅行预算:如果你想知道从广州到城市X的最便宜航班安排,最短路径算法可以帮助找出费用最少的路径。
- 优化互联网数据包路由:
- 顶点表示路由器,边表示具有不同延迟的网络连接。最短路径算法可以帮助确定延迟最小的路由路径。
- 运输问题:如果你需要找到最优的路线来减少交通延误,最短路径算法可以帮助你选择最少延迟的高速公路和道路。
- 其他应用:最短路径问题广泛应用于各种优化问题中,如物流配送、导航系统、社交网络分析等。
无权最短路径问题
问题定义:
- 问题: 给定一个源顶点 $ s $ 和一个无权有向/无向图 $ G = (V, E) $,找出从源顶点 $ s $ 到图中所有顶点的最短路径。
- 无权图: 图中的所有边没有权重,每条边的“成本”视为相等。最短路径是指通过最少的边数到达目标顶点的路径。
基本思想:
- 从源顶点 $ s $ 开始,逐步找到可以通过 0、1、2、3… $ N-1 $ 条边到达的顶点(即使是有环图也适用)。
- 使用广度优先搜索(BFS)算法来解决这个问题,因为BFS能有效地找到最短路径。
- 初始状态:
- 设置源顶点 $ s $ 的距离为 0,即 $ [s] := 0 $。
- 将源顶点 $ s $ 入队,标记它。
算法步骤:
- 初始化:
- 设置源顶点 $ s $ 的距离为 0。
- 将源顶点 $ s $ 入队 $ Q $。
- 标记源顶点 $ s $。
- 遍历过程:
- 当队列 $ Q $ 不为空时:
- 从队列中出队一个顶点 $ X $。
- 遍历 $ X $ 的所有相邻顶点 $ Y $:
- 如果 $ Y $ 尚未被标记:
- 更新 $ Y $ 的距离 $ [Y] = [X] + 1 $,即 $ Y $ 通过 $ X $ 到达。
- 如果需要记录路径,可以设置 $ [Y] := X $。
- 将 $ Y $ 入队,标记 $ Y $。
- 如果 $ Y $ 尚未被标记:
- 当队列 $ Q $ 不为空时:
- 标记:
- 一旦一个顶点被标记,它就不会再被入队一次,这避免了重复的计算。
算法伪代码:
1 | Distance[s] := 0 |
- BFS 是解决无权图最短路径问题的有效算法,因为它通过层次遍历逐渐扩展到更远的顶点,保证了找到最短路径。
- 算法的时间复杂度是 $ O(|V| + |E|) $,适用于无权图,并且能够处理图中的环。
例子(往往可以帮助做题)
注意队列里面的元素,看着元素的先后顺序就不会乱了。
加权最短路径
问题定义:
- 问题: 当图中的边有权重时,如何找到从源顶点 $ s $ 到所有其他顶点的最短路径?
- 区别: 如果边有权重,广度优先搜索(BFS)将无法再找到最短路径。因为最小代价路径可能包含比最短路径更多的边数。
Dijkstra 算法
Dijkstra 算法是解决带权图中单源最短路径问题的经典算法,但它假设所有的边权重都是非负的。
- 适用情况: Dijkstra 算法适用于带权图,且图中没有负权边。
- 算法性质: Dijkstra 算法是一种贪心算法,它通过不断选择当前最小代价的节点来扩展最短路径。
Dijkstra 算法的基本思想:
- 选择最小代价的未标记节点:
- 每次选择一个代价最小的未标记节点。
- 标记节点并更新其邻居的代价:
- 对于当前节点的每个相邻节点,检查通过当前节点的路径是否能降低该邻居的代价。如果是,则更新该邻居的代价。
- 重复上述过程,直到所有节点都被标记。
Dijkstra 算法的步骤:
- 初始化:
- 将源节点 $ s $ 的代价初始化为 0,其他节点的代价初始化为无穷大(表示不可达)。
- 初始化一个空集 $ S $,用来存储已经标记的节点,即已找到最短路径的节点。
- 执行过程:
- 当 $ S $ 中的节点数量不等于图中的所有节点时:
- 从未标记的节点中选择代价最小的节点 $ A $,并将 $ A $ 加入集合 $ S $。
- 对于节点 $ A $ 的每个相邻节点 $ B $,如果通过 $ A $ 到达 $ B $
的代价比当前已知的代价小,更新 $ B $ 的代价: $ (B) = (A) + (A, B) $
- 同时记录路径,设置 $ (B) = A $,以便我们能回溯路径。
- 当 $ S $ 中的节点数量不等于图中的所有节点时:
- 停止条件:
- 当所有节点都被标记(即所有节点都有了最短路径)时,算法结束。
伪代码:
1 | 初始化: |
例子
图论的题例子是必须的。
Dijkstra 算法实现
Dijkstra 算法伪代码:
1 | void Dijkstra(Graph* G, int* D, int s) { |
辅助函数:minVertex
minVertex
函数用于从未访问的节点中选择距离源节点最小的节点。
方法 1: 扫描所有节点找到最小距离的节点
1 | int minVertex(Graph* G, int* D) { |
复杂度分析:
- 方法 1:
minVertex
执行 $ |V| $ 次,每次扫描所有 $ |V| $ 个节点,时间复杂度为 $ (|V|^2) $。- 每次处理边的数量为 $ (|E|) $,每次访问一条边时可能会更新数组 $ D $ 的值,时间复杂度为 $ (|E|) $。
- 总体时间复杂度:$ (|V|^2 + |E|) = (|V|^2) $。
- 方法 2:
- 使用优先队列(最小堆)存储未处理的节点,按距离值排序。每次从堆中找出最小距离的节点,时间复杂度为 $ (|V|) $。
- 每次更新节点的距离时,堆会重新排序,即通过删除并重新插入来更新距离值,时间复杂度为 $ (|V|) $。
- 总体时间复杂度:$ (|E| |V|) $(处理所有边的总次数)加 $ (|V| |V|) $(每个节点的更新)。
Dijkstra 算法实现:使用优先队列
类定义:DijkElem
DijkElem
是一个用于存储节点信息的类,其中包括节点的编号(vertex
)和当前节点到源节点的距离(distance
)。
1 | class DijkElem { |
Dijkstra 算法实现:
Dijkstra 算法使用优先队列(最小堆)来选择距离最小的未访问节点,并根据当前最短路径更新邻接节点的距离。
1 | void Dijkstra(Graph* G, int* D, int s) { |
Dijkstra 算法的贪心性质:
Dijkstra 算法是一个贪心算法,它的基本思想是每次选择当前最优解(最小的距离节点),并忽略未来的可能性。具体来说,Dijkstra 算法会选择当前最小的未访问节点,但这并不意味着这种选择能保证得到全局最优解。
- 贪心策略: 在每一步中,选择当前距离最小的节点。
- 局部最优: 当前的最短路径看起来是最优的,但如果考虑后续路径,可能会有更短的路径。
- 不一定全局最优: 贪心选择并不总是最优的,尤其是当存在通过其他节点的更短路径时。
优缺点:
- 优点: Dijkstra 算法能够有效地解决无负权图的单源最短路径问题,尤其在图的规模较大时,使用优先队列能够显著提高效率。
- 缺点: 该算法的贪心策略仅考虑局部最优解,可能导致全局最优解不可达。如果图中包含负权边,则不能应用 Dijkstra 算法。
求有向图中任意两点间的最短路径长度
问题描述:
给定一个带权有向图 $ G = (V, E) $,我们要求所有节点对 $ u, v V $ 之间的最短路径长度。
解法一:使用单源最短路径算法多次运行
一种直观的做法是:对于每一对节点 $ u, v $,运行一次单源最短路径算法(如 Dijkstra 算法)。具体步骤如下:
- 对于图中的每个节点 $ u $,执行一次单源最短路径算法(例如 Dijkstra)来计算从节点 $ u $ 到所有其他节点的最短路径。
- 重复以上步骤,共运行 $ |V| $ 次(即对于每个节点都运行一次最短路径算法)。
这种方法的时间复杂度为 $ O(|V| (|E| + |V||V|)) $,其中:
- $ |V| $ 是图中的节点数。
- $ |E| $ 是图中的边数。
- $ |V||V| $ 是使用优先队列时单次 Dijkstra 算法的时间复杂度。
适用场景:
- 稀疏图: 在稀疏图中,图的边数 $ |E| $ 相对较小,Dijkstra 算法利用优先队列能够高效地求解最短路径。因此,运行 $ |V| $ 次 Dijkstra 算法能在实践中得到较好的性能。
解法二:使用全源最短路径算法
对于图中所有节点对的最短路径问题,另一种高效的算法是 Floyd-Warshall 算法,它是一个动态规划算法,能够在一个阶段内计算出所有节点对的最短路径。具体步骤如下:
- 初始化一个二维数组 $ dist[i][j] $,其中 $ dist[i][j] $ 表示从节点 $ i $ 到节点 $ j $ 的最短路径距离。
- 设置初始条件:对于每一条边 $ (i, j) E $, $ dist[i][j] = w(i, j) $,其中 $ w(i, j) $ 是边的权重;对于没有边的节点对,$ dist[i][j] $ 初始化为无穷大(除非 $ i = j $,此时 $ dist[i][i] = 0 $)。
- 通过动态规划的方式更新距离:对于每个节点 $ k $,尝试通过节点 $ k $ 来更新所有其他节点对的最短路径。 $ dist[i][j] = (dist[i][j], dist[i][k] + dist[k][j]) $
- 最终得到的 $ dist[i][j] $ 就是从节点 $ i $ 到节点 $ j $ 的最短路径长度。
代码如下:
1 | // Floyd-Warshall 算法:计算所有节点对的最短路径 |
适用场景:
- 密集图: 对于密集图,使用 Floyd-Warshall 算法通常在实践中更高效。虽然它的时间复杂度是 $ O(|V|^3) $,但在图的边数接近 $ |V|^2 $ 时,该算法的实现通常会比运行多次 Dijkstra 更快。
时间复杂度对比:
- Dijkstra 多次运行:
- 每次运行 Dijkstra 算法的时间复杂度为 $ O(|E| + |V||V|) $。
- 总的时间复杂度为 $ O(|V| (|E| + |V||V|)) $。
- Floyd-Warshall 算法:
- 时间复杂度为 $ O(|V|^3) $。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Totoroの旅!
评论