文章目录
最短路问题概述
最短路问题(Shortest Path Problem)是图论中一个经典的问题,旨在找到从一个顶点到另一个顶点的最短路径。
所给定的图可以是有向图(directed graph)也可以是无向图(undirected graph),并且边可以有权重(weights),即每条边有一个数值表示从一个顶点到另一个顶点的距离或成本。
最短路问题的常见变种包括:
- 单源最短路径问题:找到从图中某个特定顶点到所有其他顶点的最短路径。
- 单对最短路径问题:找到从图中某个特定顶点到另一个特定顶点的最短路径。
- 全源最短路径问题:找到图中每对顶点之间的最短路径。
解决最短路问题通常有包含以下算法
- Dijkstra算法:
- 定义:用于解决单源最短路径问题,适用于边权重非负的图。
- 原理:通过贪心策略,逐步选择具有最小估计距离的顶点,并更新其邻接顶点的距离。
- 时间复杂度:O((V + E) log V),其中 V 是顶点数,E 是边数。
- Bellman-Ford算法:
- 定义:用于解决单源最短路径问题,可以处理负权重的边。
- 原理:通过对所有边进行多次松弛操作,逐步逼近最短路径。
- 时间复杂度:O(VE),其中 V 是顶点数,E 是边数。
- Floyd算法:
- 定义:用于解决全源最短路径问题。
- 原理:通过动态规划的方式,逐步考虑每个顶点作为中间点,更新所有顶点对之间的最短路径。
- 时间复杂度:O(V^3),其中 V 是顶点数。
本篇讲解主要介绍Dijkstra算法的原理以及应用
带边权的图的单源最短路径
对于以下无向图,我们能够看到图中包含了若干节点以及节点之间的边权
这里的边权可以简单理解为两个节点之间的距离
大部分题目(但不是绝对的)会用一个外层长度为m
,内层长度为3
的二维数组edges
,来表无向图。
其中m
是整个图的边数,edges[i] = [start_i, end_i, w_i]
,表示在第i
条边中,start_i
节点和end_i
节点之间的距离为w_i
。
譬如,上述无向图可以用以下二维数组edges
来表示
edges = [
[0, 1, 2],
[0, 4, 8],
[1, 2, 3],
[1, 4, 2],
[2, 3, 1],
[3, 4, 1]
]
有几点需要注意:
m = len(edges) = 6
是该无向图的边数- 边
edges[i]
在edges
中的顺序并不重要- 对于无向图而言,每一条边的起点
start_i
和终点end_i
的顺序也不重要- 如果是一个有向图,可能存在一条边的起点和终点互换则边权值不相等的情况出现(在实际生活中表现为两点之间的路径存在单行道、红灯停留时间等等)
假设我们现在需要一个储存数据的结构,通过该结构能够快速查询图中从某个给定的源点出发,到任意一个点的最短距离,那么这个数据应该如何设计?
当我们想查询从源点到任意一个节点的最短距离,我们需要另一个节点的编号。因此容易想到直接构建一个一维数组或者哈希表。
假设这个给定的源点是节点0
。
我们可以把这个一维数组或者一维哈希表称为dist
,其中索引表示终点的节点编号。
dist
是distance
的缩写
如果我们要查询源点0
到任意一个节点node
的最短路径,我们直接通过查找dist[node]
的值就可以得到。
容易注意到数组或哈希表dist
存在以下特点
- 如果所有节点均连通,那么
dist
的长度为n
。n
为所有节点数。 - 如果源点为
s
,那么存在dist[s] = 0
成立,表示从节点s
到节点s
无需做任何移动,其最短距离为0
。
Dijkstra算法解决单源最短路问题
截止到目前,我们已经知道,解决单源最短路问题,要求我们得到的结果就是这个dist
。
接下来努力的方向就是如何找到这个dist
。
而Dijkstra算法就是基于贪心思想,通过多次选择下一个最近节点,找到这个dist
的过程。
dist
数组初始化
由于我们需要根据迭代逐步获得dist
数组的结果,显然可以将dist
初始化为均为正无穷inf
或者一个极大的数。
INF = 100000
dist = [INF] * n
贪心地进行dist
数组迭代
以下图为例,假设已经知道0->1
的最短路径是2
。
那么下一步我们要扩大从源点0
出发能够到达的区域,我们会如何进行选择?
如果我们把0
和1
两个节点看作是一个整体区域,那么下一步可以选择的边有0->4
,1->4
,1->2
。
如果选择0->4
,那么到达4
总花费的权重为8
。
如果选择1->4
,那么到达4
总花费的权重为4
(除了从1->4
本身的权重2
,还要算上0->1
的权重2
)。
如果选择1->2
,那么到达2
总花费的权重为5
(除了从1->2
本身的权重3
,还要算上0->1
的权重2
)。
贪心地思考这个问题,我们有两个方向的考虑。
- 如果是在
0->4
和1->4
中选出一条边,我们一定会选择1->4
,因为这样可以使得到达节点4
所花费的权重更低,这样符合最短路问题的定义。 - 如果是在
1->4
和1->2
中选出一条边,我们也一定会选择1->4
,因为我们并不清楚加上节点4
之后,是否存在包含了节点4
的路径能够使得从源点出发到2
的路径更短。
举个例子:如果此时图中存在
4->2
这条边且其权重为0
,那么0->1->4->2
的总花费为4,比0->1->2
的总花费5
要更低。
综上,我们发现选择1->4
这条权重为2
的边,能够扩大当前区域且使得到达节点4
的路径是最短的。
所以我们可以考虑,制定这样的贪心策略。
当我们已经计算完毕k
个节点的最短路,要进一步考虑第k+1
个节点时,我们总是会在未访问过的那n-k
个节点中,选择到达后总花费权重最小的那个节点来作为扩大区域的节点。
优先队列的引入
上述过程中,所谓区域扩大这件事情,实际上和BFS过程非常类似。
回忆BFS算法,我们用一个队列来维护该过程,队列中储存了若干尚未遍历过的节点。
如果从区域扩大的角度来思考BFS,在while
循环中,我们每次都会弹出队列中的队头元素,来作为当前节点。
而当前节点的(尚未检查过的)近邻点,都会再次加入队列中。
Dijkstra算法也是类似。每一次扩大区域,纳入新的节点后,都会引入一些新的边。
换言之,我们需要用一个类似队列的结构,来储存每一个新节点的近邻点的情况。
那么,我们如何能够做到,每一次扩大区域时,都能选出使得到达后总花费权重最小的节点呢?
很显然这里涉及到了一个优先级的问题,所以我们考虑使用优先队列(或者堆),来代替队列储存节点情况。
dist
数组完整迭代过程
考虑一个小根堆heap
,堆中储存若干二元组(time, node)
,表示从源点s
出发,经过某些路径,到达node
的最短路径为time
。
显然,最开始的区域只包含源点s
,所以我们可以这样初始化heap
。
heap = [(0, s)]
以上述本文的例子为例,heap
中存入(0, 0)
。
将堆顶元素(0, 0)
弹出,考虑节点0
的两个其近邻点1
和4
。
到达1
所花费的权重为0+2=2
,将(2, 1)
存入堆中。
到达4
所花费的权重为0+8=8
,将(8, 4)
存入堆中。
将堆顶元素(2, 1)
弹出。表示到达节点1
的最短路径为2
。
显然,当我们进行扩大区域的节点选择时候,会贪心地选择节点1
作为下一个节点。
我们修改dist
数组中节点1
对应的最短距离为2
。
考虑节点1
的两个近邻点2
和4
。由于到达节点1
的最短路径为2
。
经过1
到达2
所花费的权重为2+3=5
,将(5, 2)
存入堆中。
经过1
到达2
所花费的权重为2+2=4
,将(4, 4)
存入堆中。
将堆顶元素(4, 4)
弹出。表示到达节点4
的最短路径为4
。
显然,当我们进行扩大区域的节点选择时候,会贪心地选择节点4
作为下一个节点。
我们修改dist
数组中节点4
对应的最短距离为4
。
考虑节点4
的一个近邻点3
。由于到达节点4
的最短路径为4
。
经过4
到达3
所花费的权重为4+1=5
,将(5, 3)
存入堆中。
将堆顶元素(5, 2)
弹出。表示到达节点2
的最短路径为5
。
在这一步中,到达节点
2
和节点3
的距离均为5
,其实选择其中任意一个都是正确的。但由于我们小根堆中的元素是二元组,当第一个值time
相等时,会根据第二个值node
来进行大小排序。因此此处选择(5, 2)
。
显然,当我们进行扩大区域的节点选择时候,会贪心地选择节点2
作为下一个节点。
我们修改dist
数组中节点2
对应的最短距离为5
。
考虑节点2
的一个近邻点3
。由于到达节点2
的最短路径为5
。
经过2
到达3
所花费的权重为6+1=5
,将(6, 3)
存入堆中。
将堆顶元素(5, 3)
弹出。表示到达节点3
的最短路径为5
。
我们修改dist
数组中节点3
对应的最短距离为5
。
此时已经完成了所有节点的最短路径的计算。
节点3
的引入没有带来更多新增的近邻节点,因此不再更新堆。
注意到,虽然已经找到了所有节点的最短路径,但此时堆中(可能)还存在一些元素。
但显然,由于我们使用小根堆维护上述过程,每次都贪心地选择当前路径最短的节点来作为新的节点。
因此堆中的元素中的路径长度,一定不会大于dist
数组中的对应值。
剩余操作只需要将heap
中的元素全部弹出即可。
类似BFS,我们可以写出如下的代码。
Dijkstra算法完整代码
python
# 导入堆操作的库,heappush用于将元素压入堆中,heappop用于从堆中弹出最小元素
from heapq import heappush, heappop
# 导入默认值字典,用于存储图的邻接表
from collections import defaultdict
# 设置一个非常大的值,表示尚未访问的节点距离
INF = 100000
# 构建使用Dijkstra算法计算从源点s出发到达其他所有点的最短路径数组
# s是源点,n是节点数量,edges是边列表,每个边由三元组(a, b, w)表示,表示a到b的权重为w
def Dijkstra(n, edges, s):
# 初始化距离数组dist,将所有节点的初始距离设置为INF(无穷大)
dist = [INF] * n
# 初始化邻接表,默认值为列表
neighbor_dic = defaultdict(list)
# 构建邻接表,图中每个节点都与其邻居节点及边的权重进行关联
for a, b, w in edges:
# a -> b的边,权重为w
neighbor_dic[a].append((b, w))
# 如果是无向图,b -> a的边,权重同样为w
neighbor_dic[b].append((a, w)) # 如果是有向图,则这一行不需要写
# 初始化堆,将源点s的距离(0)和源点加入堆中
heap = [(0, s)]
# 进行Dijkstra算法过程,退出循环的条件为heap为空
while heap:
# 弹出堆中距离源点最近的节点及其距离
cur_time, cur_node = heappop(heap)
# 如果当前节点的距离比记录的距离更大
# 说明已经有更短的路径经过该节点,跳过该节点
if cur_time > dist[cur_node]:
continue
# 更新当前节点的最短距离
dist[cur_node] = cur_time
# 遍历当前节点的所有邻居节点
for next_node, weight in neighbor_dic[cur_node]:
next_time = cur_time + weight # 计算从当前节点到邻居节点的距离
# 如果从当前节点到邻居节点的距离比已知的最短距离更短,更新该距离并将邻居节点加入堆中
if next_time < dist[next_node]:
# 将更新后的距离和邻居节点压入堆中
heappush(heap, (next_time, next_node))
return dist # 返回从源点到所有节点的最短距离
# 初始化n和edges数组,以及源点s,可以自行进行修改和调整
n = 5
edges = [
[0, 1, 2],
[0, 4, 8],
[1, 2, 3],
[1, 4, 2],
[2, 3, 1],
[3, 4, 1]
]
s = 0
# 调用Dijkstra函数得到dist最小距离数组,输出结果
dist = Dijkstra(n, edges, s)
print(dist)
java
import java.util.*;
// 构建使用Dijkstra算法计算从源点s出发到达其他所有点的最短路径数组
public class Dijkstra {
static final int INF = 100000; // 设置一个非常大的值,表示尚未访问的节点距离
public static int[] dijkstra(int n, int[][] edges, int s) {
// 初始化距离数组dist,将所有节点的初始距离设置为INF(无穷大)
int[] dist = new int[n];
Arrays.fill(dist, INF);
// 初始化邻接表,默认值为ArrayList
Map<Integer, List<int[]>> neighborDic = new HashMap<>();
for (int i = 0; i < n; i++) {
neighborDic.put(i, new ArrayList<>());
}
// 构建邻接表,图中每个节点都与其邻居节点及边的权重进行关联
for (int[] edge : edges) {
int a = edge[0], b = edge[1], w = edge[2];
// a -> b的边,权重为w
neighborDic.get(a).add(new int[]{b, w});
// 如果是无向图,b -> a的边,权重同样为w
neighborDic.get(b).add(new int[]{a, w}); // 如果是有向图,则这一行不需要写
}
// 初始化优先队列,将源点s的距离(0)和源点加入队列
PriorityQueue<int[]> heap = new PriorityQueue<>(Comparator.comparingInt(a -> a[0]));
heap.offer(new int[]{0, s});
// 进行Dijkstra算法过程,退出循环的条件为队列为空
while (!heap.isEmpty()) {
// 弹出队列中距离源点最近的节点及其距离
int[] cur = heap.poll();
int curTime = cur[0], curNode = cur[1];
// 如果当前节点的距离比记录的距离更大
// 说明已经有更短的路径经过该节点,跳过该节点
if (curTime > dist[curNode]) {
continue;
}
// 更新当前节点的最短距离
dist[curNode] = curTime;
// 遍历当前节点的所有邻居节点
for (int[] neighbor : neighborDic.get(curNode)) {
int nextNode = neighbor[0], weight = neighbor[1];
int nextTime = curTime + weight; // 计算从当前节点到邻居节点的距离
// 如果从当前节点到邻居节点的距离比已知的最短距离更短,更新该距离并将邻居节点加入队列
if (nextTime < dist[nextNode]) {
// 将更新后的距离和邻居节点压入队列
heap.offer(new int[]{nextTime, nextNode});
}
}
}
return dist; // 返回从源点到所有节点的最短距离
}
public static void main(String[] args) {
// 初始化n和edges数组,以及源点s,可以自行进行修改和调整
int n = 5;
int[][] edges = {
{0, 1, 2},
{0, 4, 8},
{1, 2, 3},
{1, 4, 2},
{2, 3, 1},
{3, 4, 1}
};
int s = 0;
// 调用Dijkstra函数得到dist最小距离数组
int[] dist = dijkstra(n, edges, s);
System.out.println(Arrays.toString(dist));
}
}
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <unordered_map>
#include <limits.h>
using namespace std;
// 设置一个非常大的值,表示尚未访问的节点距离
const int INF = 100000;
// 构建使用Dijkstra算法计算从源点s出发到达其他所有点的最短路径数组
// s是源点,n是节点数量,edges是边列表,每个边由三元组(a, b, w)表示,表示a到b的权重为w
vector<int> Dijkstra(int n, vector<vector<int>>& edges, int s) {
// 初始化距离数组dist,将所有节点的初始距离设置为INF(无穷大)
vector<int> dist(n, INF);
// 初始化邻接表,默认值为列表
unordered_map<int, vector<pair<int, int>>> neighbor_dic;
// 构建邻接表,图中每个节点都与其邻居节点及边的权重进行关联
for (auto& edge : edges) {
int a = edge[0], b = edge[1], w = edge[2];
// a -> b的边,权重为w
neighbor_dic[a].emplace_back(b, w);
// 如果是无向图,b -> a的边,权重同样为w
neighbor_dic[b].emplace_back(a, w); // 如果是有向图,则这一行不需要写
}
// 初始化优先队列,将源点s的距离(0)和源点加入队列
// 在cpp中,默认的 priority_queue 是最大堆,
// 因此我们使用 greater<pair<int, int>> 来实现最小堆
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> heap;
heap.push({0, s});
// 进行Dijkstra算法过程,退出循环的条件为队列为空
while (!heap.empty()) {
// 弹出队列中距离源点最近的节点及其距离
auto [cur_time, cur_node] = heap.top();
heap.pop();
// 如果当前节点的距离比记录的距离更大
// 说明已经有更短的路径经过该节点,跳过该节点
if (cur_time > dist[cur_node]) {
continue;
}
// 更新当前节点的最短距离
dist[cur_node] = cur_time;
// 遍历当前节点的所有邻居节点
for (auto& neighbor : neighbor_dic[cur_node]) {
int next_node = neighbor.first, weight = neighbor.second;
int next_time = cur_time + weight; // 计算从当前节点到邻居节点的距离
// 如果从当前节点到邻居节点的距离比已知的最短距离更短,更新该距离并将邻居节点加入队列
if (next_time < dist[next_node]) {
// 将更新后的距离和邻居节点压入队列
heap.push({next_time, next_node});
}
}
}
return dist; // 返回从源点到所有节点的最短距离
}
int main() {
// 初始化n和edges数组,以及源点s,可以自行进行修改和调整
int n = 5;
vector<vector<int>> edges = {
{0, 1, 2},
{0, 4, 8},
{1, 2, 3},
{1, 4, 2},
{2, 3, 1},
{3, 4, 1}
};
int s = 0;
// 调用Dijkstra函数得到dist最小距离数组
vector<int> dist = Dijkstra(n, edges, s);
// 输出结果
for (int d : dist) {
cout << d << " ";
}
cout << endl;
return 0;
}
时空复杂度分析
假设图中的节点数为V
,边数为E
。
在Dijkstra算法中,主要的操作包括:
- 初始化:将所有节点的初始距离设为无穷大,源节点的距离设为
0
。这一步需要O(V)
的时间。 - 插入操作(heappush):每次将节点及其距离加入优先队列,时间复杂度为
O(logV)
。 - 提取最小元素(heappop):从优先队列中提取距离最小的节点,时间复杂度为
O(logV)
。
在while
循环中,我们需要如下两个过程。
提取最小距离节点操作:
- 每次从优先队列中提取当前距离最小的节点,由于一共有
V
个节点,而每个节点必须访问一次,都要至少出队一次,所以总共需要进行V
次操作。 - 每次提取操作的时间复杂度为
O(logV)
,因此这部分的总时间复杂度为O(VlogV)
。
插入近邻节点操作:
- 每个节点的所有近邻点都有可能加入优先队列中,由于一共有
E
条边,每条边的情况都有可能加入优先队列中,这部分的总操作次数与边的数量E
有关。 - 对于每条边进行一次该操作,每次都需要更新优先队列中的值,时间复杂度为
O(logV)
。 - 因此,插入近邻节点操作的总时间复杂度为
O(ElogV)
。
时间复杂度:O((V+E)logV)
。由上述两部分相加得到。
空间复杂度:O(V+E)
。堆的空间复杂度为O(V)
,邻接表的空间复杂度为O(V+E)
。
Dijkstra算法与BFS算法对比
我们可以看出Dijkstra算法和BFS算法具有非常深刻的联系。
BFS算法 | Dijkstra算法 | ||
---|---|---|---|
相同点1 | 搜索依据 | 都从一个起点开始,逐步扩展到图中的其他节点。 | BFS按层次扩展,Dijkstra算法则按最短路径优先扩展。 |
相同点2 | 数据结构 | 都使用了类似队列的数据结构。 | BFS使用普通队列,而Dijkstra使用优先队列,来确定当前处理的节点。 |
相同点3 | 路径发现 | BFS和Dijkstra都可以找到从起点出发的最短路径。 | 最短路径的定义根据是否存在边权值来改变。 |
不同点1 | 适用的图类型 | 适用于无权图或所有边权重相等的图 | 适用于权重非负的加权图 |
不同点2 | 处理顺序 | 按层次处理节点,所有距离为1的节点首先被访问,然后是距离为2的节点,以此类推。 | 按照当前已知的最短路径处理节点,优先处理距离源点最近的节点,这一顺序是基于贪心策略的。 |
关系与联系 | 如果在Dijkstra算法中,将所有边的权重设置为相同(如都为 1),那么Dijkstra算法的行为实际上与BFS相同。 | 这意味着在无权图上,BFS可以看作是Dijkstra算法的一种特殊情况 |
从代码层面上,如果我们设置所有边权值均为1,那么我们可以写出这样的BFS代码,并与Djikstra代码进行对比。
# 导入堆操作的库,heappush用于将元素压入堆中,heappop用于从堆中弹出最小元素
from heapq import heappush, heappop
# 导入默认值字典,用于存储图的邻接表
from collections import defaultdict
# 设置一个非常大的值,表示尚未访问的节点距离
INF = 100000
# 构建使用Dijkstra算法计算从源点s出发到达其他所有点的最短路径数组
# s是源点,n是节点数量,edges是边列表,每个边由三元组(a, b, w)表示,表示a到b的权重为w
def Dijkstra(n, edges, s):
# 初始化距离数组dist,将所有节点的初始距离设置为INF(无穷大)
dist = [INF] * n
# 初始化邻接表,默认值为列表
neighbor_dic = defaultdict(list)
# 构建邻接表,图中每个节点都与其邻居节点及边的权重进行关联
for a, b, w in edges:
# a -> b的边,权重为w
neighbor_dic[a].append((b, w))
# 如果是无向图,b -> a的边,权重同样为w
neighbor_dic[b].append((a, w)) # 如果是有向图,则这一行不需要写
# 初始化堆,将源点s的距离(0)和源点加入堆中
heap = [(0, s)]
# 进行Dijkstra算法过程,退出循环的条件为heap为空
while heap:
# 弹出堆中距离源点最近的节点及其距离
cur_time, cur_node = heappop(heap)
# 如果当前节点的距离比记录的距离更大
# 说明已经有更短的路径经过该节点,跳过该节点
if cur_time > dist[cur_node]:
continue
# 更新当前节点的最短距离
dist[cur_node] = cur_time
# 遍历当前节点的所有邻居节点
for next_node, weight in neighbor_dic[cur_node]:
next_time = cur_time + weight # 计算从当前节点到邻居节点的距离
# 如果从当前节点到邻居节点的距离比已知的最短距离更短,更新该距离并将邻居节点加入堆中
if next_time < dist[next_node]:
# 将更新后的距离和邻居节点压入堆中
heappush(heap, (next_time, next_node))
return dist # 返回从源点到所有节点的最短距离
# 初始化n和edges数组,以及源点s,可以自行进行修改和调整
n = 5
edges = [
[0, 1, 2],
[0, 4, 8],
[1, 2, 3],
[1, 4, 2],
[2, 3, 1],
[3, 4, 1]
]
s = 0
# 调用Dijkstra函数得到dist最小距离数组,输出结果
dist = Dijkstra(n, edges, s)
print(dist)
# 导入双端队列
from collections import deque
# 导入默认值字典,用于存储图的邻接表
from collections import defaultdict
# 设置一个非常大的值,表示尚未访问的节点距离
INF = 100000
# 构建使用BFS算法计算从源点s出发到达其他所有点的最短路径数组
# s是源点,n是节点数量,edges是边列表,每个边由(a, b)表示,表示a到b的无权边
def BFS(n, edges, s):
# 初始化距离数组dist,将所有节点的初始距离设置为INF(无穷大)
# dist数组实际上也起到checklist的作用,
# 为了统一写法,这里不使用我们常见的checklist来取变量名
dist = [INF] * n
# 初始化邻接表,默认值为列表
neighbor_dic = defaultdict(list)
# 构建邻接表,图中每个节点都与其邻居节点进行关联
for a, b in edges:
# a -> b的边
neighbor_dic[a].append(b)
# b -> a的边
neighbor_dic[b].append(a) # 如果是有向图,则这一行不需要写
# 初始化队列,将源点s加入队列
# 不用加入距离,因为不存在边权
queue = deque([s])
dist[s] = 0 # 源点到自己的距离为0,类似于checklist的初始化
# 进行BFS算法过程,退出循环的条件为队列为空
while queue:
# 弹出队列中的节点
cur_node = queue.popleft()
# 由于不存在边权
# 此时出队的节点必然是先遇到的节点
# 同时,我们也不需要出队的时候进行dist的修改
# 而是在的时候进行dist的修改
# 此处无需像Dijkstra写额外的检查和更新
# 遍历当前节点的所有邻居节点
for next_node in neighbor_dic[cur_node]:
# 如果当前节点到邻居节点的距离更短,更新该距离并将邻居节点加入队列
# 此处dist[next_node] == INF也可以认为是,表示next_node尚未访问过
if dist[next_node] == INF:
dist[next_node] = dist[cur_node] + 1 # 无权图中,每条边的距离为1
queue.append(next_node) # 将更新后的邻居节点加入队列
return dist # 返回从源点到所有节点的最短距离
# 初始化n和edges数组,以及源点s,可以自行进行修改和调整
n = 5
# 将无权图的边列表初始化为没有权重的形式
edges = [
[0, 1],
[0, 4],
[1, 2],
[1, 4],
[2, 3],
[3, 4]
]
s = 0 # 源点
# 调用BFS函数得到dist最小距离数组,输出结果
dist = BFS(n, edges, s)
print(dist)
*Dijkstra算法正确性证明
本处内容不做具体讲解,感兴趣的同学可以自行学习。
Dijkstra算法的正确性可以通过归纳法和贪心策略来证明。
贪心策略
Dijkstra算法之所以能够正确地找到最短路径,关键在于它的贪心选择性质。
每次选择当前未访问节点中距离源点最短的节点u
,并更新其邻居的最短距离。
这个性质确保了从源点s
到所选节点u
的路径是最短的。
可以使用反证法来证明这个贪心策略的正确性。
因为如果从s
到u
的路径不是最短的,那么至少存在一条更短的路径经过某个未访问的节点 v
。但在这种情况下,节点v
应该在u
之前被选中,这与假设u
是距离最短的节点相矛盾。
因此,当一个节点被从优先队列中弹出并标记为已访问时,它的距离是正确的。
归纳法证明
- 基础情况:
- 当只考虑源点
s
时,算法正确地将dist[s]
设置为0
,所有其他节点的距离为inf
。
- 当只考虑源点
- 归纳假设:
- 假设对于前
k
个已访问的节点,算法已经正确计算出从源点到这些节点的最短路径。
- 假设对于前
- 归纳步骤:
- 现在考虑第
k+1
个节点u
。根据贪心策略,u
是优先队列中距离源点最近的节点。 - 由于之前所有节点的最短路径已经计算正确,并且
u
是当前最近的节点,所以从s
到u
的最短路径一定经过这些已经访问过的节点,且距离已经最短。 - 因此,更新
u
的邻居的距离也是正确的。
- 现在考虑第
通过这种方式,归纳法证明了 Dijkstra 算法在所有节点上都能正确找到最短路径。