单源最短路问题:Dijkstra算法详解【经典算法,限时免费】

最短路问题概述

最短路问题(Shortest Path Problem)是图论中一个经典的问题,旨在找到从一个顶点到另一个顶点的最短路径。

所给定的图可以是有向图(directed graph)也可以是无向图(undirected graph),并且边可以有权重(weights),即每条边有一个数值表示从一个顶点到另一个顶点的距离或成本。

最短路问题的常见变种包括:

  1. 单源最短路径问题:找到从图中某个特定顶点到所有其他顶点的最短路径。
  2. 单对最短路径问题:找到从图中某个特定顶点到另一个特定顶点的最短路径。
  3. 全源最短路径问题:找到图中每对顶点之间的最短路径。

解决最短路问题通常有包含以下算法

  1. Dijkstra算法
  • 定义:用于解决单源最短路径问题,适用于边权重非负的图。
  • 原理:通过贪心策略,逐步选择具有最小估计距离的顶点,并更新其邻接顶点的距离。
  • 时间复杂度:O((V + E) log V),其中 V 是顶点数,E 是边数。
  1. Bellman-Ford算法
  • 定义:用于解决单源最短路径问题,可以处理负权重的边。
  • 原理:通过对所有边进行多次松弛操作,逐步逼近最短路径。
  • 时间复杂度:O(VE),其中 V 是顶点数,E 是边数。
  1. 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]
]

有几点需要注意:

  1. m = len(edges) = 6 是该无向图的边数
  2. edges[i]edges中的顺序并不重要
  3. 对于无向图而言,每一条边的起点start_i和终点end_i的顺序也不重要
  4. 如果是一个有向图,可能存在一条边的起点和终点互换则边权值不相等的情况出现(在实际生活中表现为两点之间的路径存在单行道、红灯停留时间等等)

假设我们现在需要一个储存数据的结构,通过该结构能够快速查询图中从某个给定的源点出发,到任意一个点的最短距离,那么这个数据应该如何设计?

当我们想查询从源点到任意一个节点的最短距离,我们需要另一个节点的编号。因此容易想到直接构建一个一维数组或者哈希表。

假设这个给定的源点是节点0

在这里插入图片描述

我们可以把这个一维数组或者一维哈希表称为dist,其中索引表示终点的节点编号。

distdistance的缩写

如果我们要查询源点0到任意一个节点node的最短路径,我们直接通过查找dist[node]的值就可以得到。

容易注意到数组或哈希表dist存在以下特点

  1. 如果所有节点均连通,那么dist的长度为nn为所有节点数。
  2. 如果源点为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出发能够到达的区域,我们会如何进行选择?

如果我们把01两个节点看作是一个整体区域,那么下一步可以选择的边有0->41->41->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)。

贪心地思考这个问题,我们有两个方向的考虑。

  1. 如果是在0->41->4中选出一条边,我们一定会选择1->4,因为这样可以使得到达节点4所花费的权重更低,这样符合最短路问题的定义。
  2. 如果是在1->41->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的两个其近邻点14

到达1所花费的权重为0+2=2,将(2, 1)存入堆中。

到达4所花费的权重为0+8=8,将(8, 4)存入堆中。

暂时无法在飞书文档外展示此内容


将堆顶元素(2, 1)弹出。表示到达节点1的最短路径为2

显然,当我们进行扩大区域的节点选择时候,会贪心地选择节点1作为下一个节点。

我们修改dist数组中节点1对应的最短距离为2

考虑节点1的两个近邻点24。由于到达节点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算法中,主要的操作包括:

  1. 初始化:将所有节点的初始距离设为无穷大,源节点的距离设为0。这一步需要O(V)的时间。
  2. 插入操作(heappush):每次将节点及其距离加入优先队列,时间复杂度为O(log⁡V)
  3. 提取最小元素(heappop):从优先队列中提取距离最小的节点,时间复杂度为O(log⁡V)

while循环中,我们需要如下两个过程。

提取最小距离节点操作

  • 每次从优先队列中提取当前距离最小的节点,由于一共有V个节点,而每个节点必须访问一次,都要至少出队一次,所以总共需要进行V次操作。
  • 每次提取操作的时间复杂度为O(log⁡V),因此这部分的总时间复杂度为O(Vlog⁡V)

插入近邻节点操作

  • 每个节点的所有近邻点都有可能加入优先队列中,由于一共有E条边,每条边的情况都有可能加入优先队列中,这部分的总操作次数与边的数量E有关。
  • 对于每条边进行一次该操作,每次都需要更新优先队列中的值,时间复杂度为O(log⁡V)
  • 因此,插入近邻节点操作的总时间复杂度为O(Elog⁡V)

时间复杂度: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的路径是最短的。

可以使用反证法来证明这个贪心策略的正确性。

因为如果从su的路径不是最短的,那么至少存在一条更短的路径经过某个未访问的节点 v。但在这种情况下,节点v应该在u之前被选中,这与假设u是距离最短的节点相矛盾。

因此,当一个节点被从优先队列中弹出并标记为已访问时,它的距离是正确的。

归纳法证明

  • 基础情况
    • 当只考虑源点s时,算法正确地将dist[s]设置为0,所有其他节点的距离为inf
  • 归纳假设
    • 假设对于前k个已访问的节点,算法已经正确计算出从源点到这些节点的最短路径。
  • 归纳步骤
    • 现在考虑第k+1个节点u。根据贪心策略,u是优先队列中距离源点最近的节点。
    • 由于之前所有节点的最短路径已经计算正确,并且u是当前最近的节点,所以从su的最短路径一定经过这些已经访问过的节点,且距离已经最短。
    • 因此,更新u的邻居的距离也是正确的。

通过这种方式,归纳法证明了 Dijkstra 算法在所有节点上都能正确找到最短路径。

相关题目

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

闭着眼睛学算法

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值