最短路径算法

1. Dijkstra算法

在正数权重的有向图中求解某个源点其余各个顶点的最短路径一般可以采用迪杰斯特拉算法(Dijkstra算法)。

1.1 适用场景

  • 单源最短路径
  • 权重都为正

该算法为什么权重必须为正?
Dijkstra算法要求权重都为正的原因在于其贪心策略和松弛操作的性质。以下是具体原因:
1.贪心策略
Dijkstra算法每次选择当前最短路径的节点进行扩展,假设从该节点到其他节点的路径不会更短。如果存在负权重,这一假设可能不成立,因为后续的负权重可能使路径变得更短,导致算法无法正确找到最短路径。
2.松弛操作
算法通过松弛操作更新路径长度,假设路径长度只会减少或保持不变。负权重可能导致路径长度反复减少,使得算法无法在有限步骤内确定最短路径。
3.节点处理顺序
Dijkstra算法按路径长度递增的顺序处理节点,一旦节点被处理,其最短路径不再更新。负权重可能导致已处理节点的路径长度需要重新计算,破坏算法的正确性。
4.负权环问题
如果图中存在负权环,路径长度可以无限减少,Dijkstra算法无法处理这种情况。
总结
Dijkstra算法依赖权重为正的假设,确保每次选择的最短路径是全局最优的。若存在负权重,算法可能失效。对于包含负权重的图,应使用Bellman-Ford等算法。

1.2 伪代码

在这里插入图片描述
这里需要详细说明参数,v表示第一个顶点,D[i]表示从v到各个顶点vi的最短路径长度。

1.3 示例

问题描述: 计算节点0节点4的最短路径,图路径如下:
在这里插入图片描述
step1: 采用二维表记录0点到其他节点的距离,第一列距离初始化为 ∞ \infty ,第二列记录到达每个节点时,该节点前面的点,主要用于最短路径回溯。
在这里插入图片描述
step2: 0到0的距离是0,为最优路径,标记为对勾,节点0到节点1、7的距离分别为4、8,节点1、7前面的点为节点0,所以在前面点填写0。
在这里插入图片描述
step3: 在未标记的点中选取最小值,最小值为4,对应的节点为1,将其标记为最优路径,标记为对勾,计算最小值节点1的邻居节点。
在这里插入图片描述
经过最小值节点1可以到达邻居节点2、7,到节点2的距离是 4 + 8 = 12 < ∞ 4+8=12<\infty 4+8=12<,所以更新节点2的距离为12,到达节点2时,经过的前面节点为节点1。
经过最小值节点1到达节点7的距离 4 + 11 = 15 > 8 4+11=15>8 4+11=15>8,所以不更新节点7,节点7前面的节点仍为节点0。
step4: 在剩余未标记的点中选取最小值,最小值为8,对应的节点为7,将其标记为最优路径,标记为对勾,计算最小值节点7的邻居节点。
在这里插入图片描述
更新节点6、8,节点6的距离8(节点7的最短距离)+1=9和节点8的距离8(节点7的最短距离)+7=15,前面的节点都为7。以此类推,更新所有的节点。
在这里插入图片描述
最后0到所有节点的最短近距离如下:
在这里插入图片描述

1.4 计算最短路径

节点0到节点4的最短距离为21,节点4的前面节点为节点5, 5 → 4 5 \rightarrow 4 54,节点5前面的节点是节点6, 6 → 5 6 \rightarrow5 65, 节点6前面是节点7, 7 → 6 7\rightarrow6 76,节点7前面是节点0, 0 → 7 0\rightarrow7 07,综上所述,最短路径为: 0 → 7 → 6 → 5 → 4 0 \rightarrow 7 \rightarrow6 \rightarrow 5 \rightarrow 4 07654,距离为21。

1.5 Leetcode例子

https://leetcode.cn/problems/network-delay-time/solutions/909575/wang-luo-yan-chi-shi-jian-by-leetcode-so-6phc/

#include <iostream>
#include <vector>
#include <queue>
#include <limits>

using namespace std;

const int INF = numeric_limits<int>::max();

vector<int> dijkstra(const vector<vector<pair<int, int>>>& graph, int source) {
    int n = graph.size();
    vector<int> dist(n, INF);
    vector<bool> visited(n, false);
    priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
    
    dist[source] = 0;
    pq.push({0, source});
    
    while (!pq.empty()) {
        int u = pq.top().second;
        pq.pop();
        
        if (visited[u]) continue;
        visited[u] = true;
        
        for (auto& edge : graph[u]) {
            int v = edge.first;
            int weight = edge.second;
            if (dist[u] + weight < dist[v]) {
                dist[v] = dist[u] + weight;
                pq.push({dist[v], v});
            }
        }
    }
    
    return dist;
}

int main() {
    vector<vector<pair<int, int>>> graph = {
        {{1, 4}, {2, 2}},
        {{3, 3}},
        {{1, 1}, {3, 5}},
        {}
    };

    int source = 0;
    vector<int> distances = dijkstra(graph, source);

    cout << "Shortest distances from node " << source << ":" << endl;
    for (int i = 0; i < distances.size(); ++i) {
        cout << "Node " << i << ": " << distances[i] << endl;
    }

    return 0;
}

在这个示例中,我们定义了一个 dijkstra() 函数来执行 Dijkstra 算法。该函数接受一个邻接表表示的图和源节点作为输入,并返回从源节点每个节点的最短路径距离。

1.6 输出最短路径

如果存在多条最短路径就输出多条最短路径

import heapq
from collections import defaultdict

def dijkstra(graph, start, end):
    # 优先队列,存储 (距离, 当前节点, 路径)
    pq = [(0, start, [start])]
    # 记录每个节点的最短距离
    shortest_distances = {start: 0}
    # 记录所有最短路径
    shortest_paths = defaultdict(list)

    while pq:
        current_dist, current_node, path = heapq.heappop(pq)

        # 如果当前节点是终点,记录路径
        if current_node == end:
            shortest_paths[current_dist].append(path)
            continue

        # 如果当前距离大于已知的最短距离,跳过
        if current_dist > shortest_distances.get(current_node, float('inf')):
            continue

        # 遍历邻居节点
        for neighbor, weight in graph[current_node].items():
            distance = current_dist + weight
            # 如果找到更短的路径
            if distance < shortest_distances.get(neighbor, float('inf')):
                shortest_distances[neighbor] = distance
                heapq.heappush(pq, (distance, neighbor, path + [neighbor]))
            # 如果找到等长的路径
            elif distance == shortest_distances.get(neighbor, float('inf')):
                heapq.heappush(pq, (distance, neighbor, path + [neighbor]))

    # 如果没有找到路径
    if not shortest_paths:
        return None, None

    # 获取最短距离
    min_distance = min(shortest_paths.keys())
    # 获取所有最短路径
    all_shortest_paths = shortest_paths[min_distance]

    return min_distance, all_shortest_paths

# 示例图
graph = {
    'A': {'B': 1, 'C': 4},
    'B': {'A': 1, 'C': 2, 'D': 5},
    'C': {'A': 4, 'B': 2, 'D': 1},
    'D': {'B': 5, 'C': 1}
}

start = 'A'
end = 'D'

# 运行算法
distance, paths = dijkstra(graph, start, end)

# 输出结果
if distance is not None:
    print(f"最短距离: {distance}")
    print("所有最短路径:")
    for path in paths:
        print(" -> ".join(path))
else:
    print("没有找到路径")

2. Bellman-Ford算法

Bellman-ford算法适用于单源最短路径,图中边的权重可为负数即负权边,但不可以出现负权环。

2.1 适用场景

  • 单源最短路径
  • 边的权重可为负数即负权边
  • 不可以出现负权环

2.2 伪代码

在这里插入图片描述

2.3 示例

step1: 初始图如下,箭头上的数字表示权重,括号内容含义为:(前面节点,距离),除去源点,其他点的初始距离为 ∞ \infty 1 ◯ − 7 ◯ \textcircled{1}-\textcircled{7} 17表示边的编号。接下来,从节点0到节点n-1开始遍历。
在这里插入图片描述
step2:节点0出发:更新所有节点的权重,节点 0 → 1 0 \rightarrow 1 01为90, 0 → 4 0 \rightarrow 4 04为75, 0 → 3 0 \rightarrow 3 03为80,它们的距离都小于 ∞ \infty ,到达节点的上一个节点都是节点0,更新为(0,距离)。
在这里插入图片描述

step3:节点1出发, 0 → 1 → 2 0 \rightarrow 1 \rightarrow 2 012 90 − 30 = 60 < ∞ 90-30=60<\infty 9030=60<, 由于节点2上一个节点是节点1,所以标记为(1,60)。
在这里插入图片描述
step3:节点2出发,60+10<75,节点4更新为(2,70)
在这里插入图片描述
step4:节点3出发,80-30=50<60,更新节点2为(3,50)。80+10=90>70,无需更新节点4。
在这里插入图片描述
step4:节点4出发,不存在边,无需更新,完成第一遍所有的松弛操作。

step5: 进行第二轮遍历,由于节点 2 → 4 2 \rightarrow 4 24 的和50+10=60<70,更新节点4为(2,60)。
在这里插入图片描述

step6: 第三遍遍历没有任何松弛操作,且3<=n-1=4,说明不存在负权重的环,可以直接返回节点0到其他节点的距离。如果存在松弛操作,最多进行n-1轮遍历,如果第n轮还存在松弛操作,说明存在负权重的环。
最短路径如下图红框所示:

2.4 计算最短路径

在这里插入图片描述

2.5 补充

如果存在负环,示例如下:
在这里插入图片描述
节点2、3、4存在负环,负环会导致无穷迭代。
第1遍遍历结果
在这里插入图片描述
更新节点4为(2,60)
在这里插入图片描述
更新节点3为(4,70)
在这里插入图片描述
更新节点2为(3,40),以此类推,导致无穷迭代。
在这里插入图片描述
Bellman-Ford 算法最厉害的地方在于它能处理带有负权边的图,而且还能顺便帮你检测出图中是否存在负权环。它的核心逻辑其实特别直观:对图中的所有边进行 n-1 轮松弛操作,每一轮都尝试更新从起点到各个点的最短距离。

下面我用 Python 和 C++ 分别为你实现一个标准的 Bellman-Ford 算法:

2.5.1Python 实现

def bellman_ford(n, edges, start):
    """
    n: 节点数量 (节点编号假设为 0 到 n-1)
    edges: 边的列表,格式为 [(u, v, w), ...],表示从 u 到 v 的边权重为 w
    start: 起点
    返回: 从起点到各点的最短距离字典,如果存在负权环则返回 None
    """
    # 初始化距离数组,起点为0,其余为无穷大
    INF = float('inf')
    dist = [INF] * n
    dist[start] = 0

    # 1. 进行 n-1 轮松弛操作
    for i in range(n - 1):
        updated = False
        for u, v, w in edges:
            # 如果当前起点距离不是无穷大,且通过 u 到 v 能获得更短的路径
            if dist[u] != INF and dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                updated = True
        # 如果某一轮没有任何更新,说明已经找到所有最短路,可以提前退出
        if not updated:
            break

    # 2. 检测负权环
    # 再遍历一次所有边,如果还能松弛,说明图中存在负权环
    for u, v, w in edges:
        if dist[u] != INF and dist[u] + w < dist[v]:
            print("图中存在负权环!")
            return None

    return dist

# --- 测试代码 ---
# 假设有 5 个节点 (0-4)
n = 5
edges = [
    (0, 1, 6), (0, 2, 7), (1, 2, 8), (1, 3, 5), 
    (1, 4, -4), (2, 3, -3), (2, 4, 9), 
    (3, 1, -2), (4, 0, 2), (4, 3, 7)
]
start_node = 0

distances = bellman_ford(n, edges, start_node)

if distances:
    print(f"从节点 {start_node} 到各点的最短距离为:")
    for i, d in enumerate(distances):
        print(f"节点 {i}: {d}")

核心要点总结

  1. 时间复杂度:O(V*E),其中 V 是顶点数,E 是边数。
  2. 适用场景:适用于有负权边的图。如果图中没有负权边,通常使用效率更高的 Dijkstra 算法。
  3. 负权环检测:算法通过第 n 次遍历来判断。如果在 n-1 轮松弛后,依然能进行松弛操作,说明图中存在一个环,且绕着这个环走一圈会让路径总权重变得更小(即负权环),此时最短路径是没有意义的(可以无限小)。
  4. 提前退出优化:代码中加入了一个 updated 标志位。如果某一轮遍历中没有任何距离被更新,说明所有最短路已经收敛,可以直接跳出循环,这在很多情况下能提升实际运行速度。

2.5.2 为什么N-1轮优化?

这其实是由“最短路径”的极限形态决定的。

在一个包含 n 个节点的图中,任意两点之间的最短路径,最多只会包含 n-1 条边。

我们可以这样来理解:

极端情况:最长的一条路

假设你有 n 个节点。如果要把这 n 个节点全部串成一条**不绕圈(无环)**的单行道,这条路最长能有多长?
答案就是:从第 1 个点出发,依次经过剩下的 n-1 个点,最终到达终点。
这条路上一共经过了 n 个节点,但连接它们的边只有 n-1 条。

算法的运作机制(逐层松弛)

Bellman-Ford 算法的每一轮遍历,本质上是在**“把最短路径的长度延长 1 条边”**:

  • 第 1 轮松弛后:你能保证找到了所有“只经过 1 条边”的最短路径。
  • 第 2 轮松弛后:你能保证找到了所有“最多经过 2 条边”的最短路径。
  • ……
  • 第 k 轮松弛后:你能保证找到了所有“最多经过 k 条边”的最短路径。

既然任何最短路径最多只有 n-1 条边,那么只要进行 n-1 轮松弛,就绝对足够让最短路径的信息从起点传递到图中的任何一个角落。

举个直观的例子

假设有 5 个节点(A, B, C, D, E),连成一条直线:
A -> B -> C -> D -> E
(这里 n = 5)

  • 起点是 A
  • 第 1 轮循环:算法发现了 A->B,确定了 B 的最短距离(1条边)。
  • 第 2 轮循环:利用 B 的距离,算法发现了 A->B->C,确定了 C 的最短距离(2条边)。
  • 第 3 轮循环:确定了 D 的最短距离(3条边)。
  • 第 4 轮循环:确定了 E 的最短距离(4条边)。

你看,5 个节点,只需要 4 轮(n-1)就能把最远的 E 算出来。再多算几轮也不会有变化了。

那为什么第 n 轮能检测负权环?

如果进行了 n-1 轮之后,你发现还能继续松弛(也就是第 n 轮还能更新距离),这说明了什么?
说明你找到了一条包含 n 条边(甚至更多)的“最短路径”。

在 n 个节点的图里,一条包含 n 条边的路径,根据抽屉原理,必然走了回头路(形成了环)

  • 如果这个环的权重是正的或零,绕路走肯定不会让距离变短。
  • 只有当这个环是负权环时,每绕一圈距离都会变小,算法才会觉得“我还能继续松弛,还能更短!”。

所以,n-1 轮是用来求最短路的,而多出来的第 n 轮是用来“抓鬼”(检测负权环)的。

2.6 参考资料

Bellman-ford算法 无向图

3 Floyd算法

Floyd算法是也称为弗洛伊德算法或插点法,是一种动态规划算法,求解多源最短路(多对多)的算法,即确定每个节点(起点)到其他节点(终点)的最短路。

3.1 适用场景

  • 求解多源最短路(多对多)的算法
  • 算法适用于有向图、无向图,允许边的权重为负,但是负边构成的回路(环)的权重之和不能为负(负环)

3.2 伪代码

在这里插入图片描述

//Floyd
//多源最短路
static int[][] g=new int[n+1][n+1];//邻接矩阵存图
static int[][][] dp=new int[n+1][n+1][n+1];
static void Floyed(){
	for(int k=0;k<=n;k++){
		for(int i=1;i<=n;i++){
			for(int j=1;j<=n;j++){
				if(k==0){
					dp[k][i][j]=g[i][j];
				}else{
					dp[k][i][j]=Math.min(dp[k-1][i][j],dp[k-1][i][k]+dp[k-1][k][j]);
				}
				
			}
		}
	}
}

3.3 示例

step 1: 初始化邻接矩阵,节点编号从1开始
在这里插入图片描述
step 2: 每一轮选取中间节点,比较 a i j a_{ij} aij a i k + a k j a_{ik}+a_{kj} aik+akj大小
在这里插入图片描述
step 3: 采用两个矩阵存储,绿色矩阵存储源节点(src)和目标节点(dst)之间的距离,黄色矩阵存储dst节点前面的节点。0表示节点直达,不存在中间节点。注意:节点的编号是从1开始的,0标号不存在冲突。
在这里插入图片描述
step 4: 从节点1作为中间节点开始遍历,主要关注 a i 1 a_{i1} ai1 a 1 j a_{1j} a1j矩阵更新结果如下:
在这里插入图片描述
step 5: 从节点2作为中间节点开始遍历,主要关注 a i 2 a_{i2} ai2 a 2 j a_{2j} a2j矩阵更新结果如下:
在这里插入图片描述
由于 A 1 [ 1 ] [ 2 ] + A 1 [ 2 ] [ 3 ] = 60 < ∞ A1[1][2]+A1[2][3]=60 \lt \infty A1[1][2]+A1[2][3]=60<,所以更新A2矩阵 A [ 1 ] [ 3 ] = 60 A[1][3]=60 A[1][3]=60,经过的节点为2,更新P2矩阵为 P 2 [ 1 ] [ 3 ] = 2 P2[1][3] = 2 P2[1][3]=2
step 6: 从节点3作为中间节点开始遍历,主要关注 a i 3 a_{i3} ai3 a 3 j a_{3j} a3j矩阵更新结果如下:
在这里插入图片描述
由于 A 2 [ 1 ] [ 3 ] + A 2 [ 3 ] [ 5 ] = 70 < 100 , A 2 [ 2 ] [ 3 ] + A 2 [ 3 ] [ 5 ] = 60 < ∞ , A 2 [ 4 ] [ 3 ] + A [ 3 ] [ 5 ] = 30 < 60 A2[1][3]+A2[3][5] = 70 < 100,A2[2][3] + A2[3][5]=60<\infty, A2[4][3] + A[3][5]=30<60 A2[1][3]+A2[3][5]=70<100,A2[2][3]+A2[3][5]=60<,A2[4][3]+A[3][5]=30<60, 所以在A3中更新 A 3 [ 1 ] [ 5 ] , A 3 [ 2 ] [ 5 ] , A 3 [ 4 ] [ 5 ] A3[1][5], A3[2][5], A3[4][5] A3[1][5],A3[2][5],A3[4][5], 在P3中更新对应的前置节点为3。
step 7: 从节点4作为中间节点开始遍历,主要关注 a i 4 a_{i4} ai4 a 4 j a_{4j} a4j矩阵更新结果如下:
在这里插入图片描述
step 8: 从节点5作为中间节点开始遍历,主要关注 a i 5 a_{i5} ai5 a 5 j a_{5j} a5j矩阵更新结果如下:
在这里插入图片描述

3.4 计算最短路径

在这里插入图片描述

  • 分析1: 1 → 2 1\rightarrow 2 12
    • 距离为 A 5 [ 1 ] [ 2 ] = 10 A5[1][2]=10 A5[1][2]=10
    • 由于 P 5 [ 1 ] [ 2 ] = 0 P5[1][2]=0 P5[1][2]=0, 表示节点2前面没有中间节点,属于直连的情况。
  • 分析2: 1 → 5 1\rightarrow 5 15
    • 距离为 A 5 [ 1 ] [ 5 ] = 60 A5[1][5]=60 A5[1][5]=60,
    • 由于 P 5 [ 1 ] [ 5 ] = 4 P5[1][5]=4 P5[1][5]=4,表示需要经过中间节点4, 1 → 4 → 5 1\rightarrow 4\rightarrow5 145
    • 递归分析 1 → 4 1\rightarrow 4 14 P 5 [ 1 ] [ 4 ] = 0 P5[1][4]=0 P5[1][4]=0, 表示节点4前面没有中间节点,属于直连的情况。
    • 递归分析 4 → 5 4\rightarrow 5 45 P 5 [ 4 ] [ 5 ] = 3 P5[4][5]=3 P5[4][5]=3,表示需要经过中间节点3, 4 → 3 → 5 4\rightarrow 3\rightarrow5 435
    • 递归分析 4 → 3 4\rightarrow 3 43 P 5 [ 4 ] [ 3 ] = 0 P5[4][3]=0 P5[4][3]=0, 表示节点3前面没有中间节点,属于直连的情况。
    • 递归分析 3 → 5 3\rightarrow 5 35 P 5 [ 3 ] [ 5 ] = 0 P5[3][5]=0 P5[3][5]=0, 表示节点5前面没有中间节点,属于直连的情况。
    • 回溯结果,最短路径最终为 1 → 4 → 3 → 5 1\rightarrow4\rightarrow 3\rightarrow 5 1435

4 SPFA 算法

SPFA 算法是 Bellman-Ford算法的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下时间复杂度和朴素 Bellman-Ford 相同,为 O(VE)。

4.1 适用场景

  • 单源最短路径
  • 边的权重可为负数即负权边
  • 不可以出现负权环

4.2 伪代码

在这里插入图片描述

4.3 示例

https://zhuanlan.zhihu.com/p/353019102

5 算法特点对比

5.1 Dijkstra 与Bellman-ford

(1)Dijkstra为贪心算法,Bellmon-Ford算法不是贪心算法
(2)Dijkstra在有负权的情况下无法工作,Bellmon-Ford算法允许有负权。
(3)Bellmon-Ford算法可以用来判定是否有负权环。
(4)从计算复杂度角度分析,单节点算法首选Dijkstra算法的,它的计算复杂度为 O ( n 2 ) O(n^2) O(n2),Bellman-ford时间复杂度为O(m*n),n表示有n个点,m表示有m条边。

Bellman-ford算法的时间复杂度是O(N*M),这个时间复杂度貌似比Dijkstra算法还要高,我们还可以对其进行优化。在实际操作中,Bellman-ford算法经常会未达到n-1轮松弛前就已经算出最短路,因此我们可以判断第k轮是否进行更新,如果不进行更新了,则可以提前跳出循环。
Bellman-ford算法的另一种优化在文中已经有所提示:在每实施一次松弛操作后,就会有一些顶点已经求其最短路。此后这些顶点的最短路的估计值就会一直保持不变,不在受后序松弛操作的影响,但是每次还需要判断是否需要松弛。这就启发我们:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。详情请看Bellman-ford的队列优化-SPAF。

5.2 Floyd与Dijkstra

Floyd算法和Dijkstra算法都是用于解决图论中最短路径问题的算法,但它们在适用范围、计算复杂度、算法原理等方面存在差异:

(1)适用范围:
Floyd算法:适用于求解图中任意两个顶点之间的最短路径,可以处理带负权边的图。
Dijkstra算法:适用于求解图中一个源点到其他所有点的最短路径,不能处理负权边的图。
(2)计算复杂度:
Floyd算法:时间复杂度为 O ( n 3 ) O(n^3) O(n3),适用于边数较少的稠密图。
Dijkstra算法:时间复杂度为 O ( n 2 ) O(n^2) O(n2),适用于边数较多的稀疏图。
(3)算法原理:
Floyd算法:是一种动态规划算法,通过定义一个距离矩阵,并更新这个矩阵,逐步找到图中任意两点之间的最短路径。
Dijkstra算法:是一种贪心算法,每次找到距离源点最近的顶点,并更新与之相连的顶点的最短路径,直到所有顶点都被遍历过一次或找到终点。
(4)算法特点:
Floyd算法:可以处理负权边,但时间复杂度较高。
Dijkstra算法:只适用于无负权边的图,时间复杂度较低。
SPFA 算法:与bfs算法比较,复杂度相对稳定。但在稠密图中复杂度比迪杰斯特拉算法差。

6. 0-1bfs和常规bfs有什么区别,为啥0-1bfs用最小堆,不是用队列就可以么?

这里其实有个小误会,0-1 BFS 最大的特色恰恰是不用最小堆(优先队列),而是只用普通的双端队列(deque)

如果用了最小堆,那其实就是 Dijkstra 算法了。0-1 BFS 正是为了在“边权只有 0 和 1”这种特殊情况下,把 Dijkstra 的复杂度降下来,才发明了这种用双端队列的巧妙方法。

我们来详细拆解一下它和常规 BFS 的区别,以及为什么要这么设计:

6.1 0-1 BFS 与常规 BFS 的核心区别

特性常规 BFS0-1 BFS
适用场景边权全部相等(通常视为 1)边权只有 0 或 1
使用的数据结构普通队列 (Queue)双端队列 (Deque)
入队方式无论走到哪,新节点一律从队尾入队边权为 0 从队首入队;边权为 1 从队尾入队
时间复杂度O(V + E)O(V + E)

6.2 为什么常规 BFS 不能直接用于 0-1 权重的图?

常规 BFS 能够保证算出最短路,有一个绝对前提:“先被访问到的节点,它的路径长度一定是最短的(或相等的)”

因为每条边权都是 1,第 1 层距离是 1,第 2 层距离是 2……像水波纹一样均匀扩散。

但在 0-1 权重的图中,这个前提失效了:
假设你当前在节点 A(距离为 5):

  • 走边权为 1 的路到了 B,B 的距离是 6。
  • 走边权为 0 的路到了 C,C 的距离依然是 5。

如果你用普通队列,把 B 和 C 都扔到队尾。下次处理时,你可能会先去处理 B(距离 6),但实际上 C(距离 5)才是更应该优先处理的。普通队列无法保证“距离小的节点先被处理”。


6.3 0-1 BFS 为什么用双端队列 (Deque)?

为了保证“距离小的先处理”,最通用的做法是用最小堆(优先队列),也就是 Dijkstra 算法。每次从堆里弹出一个距离最小的节点。但堆的操作复杂度是 O(log V)。

在 0-1 BFS 中,我们发现了一个极其巧妙的规律,可以用双端队列模拟出最小堆的效果,把复杂度降回 O(1):

  1. 遇到边权为 0 的边(距离不变)
    这就好比你原地挪了一步,距离没增加。这个新节点的距离和当前节点一样,理应和当前节点排在同一批次处理。所以,直接把它插到队首(push_front),让它紧挨着当前节点,下一个就处理它。

  2. 遇到边权为 1 的边(距离 +1)
    这就好比常规 BFS 的下一层。这个新节点的距离比当前节点大 1,应该放到后面去排队。所以,把它扔到队尾(push_back)

通过这种“0 插队首,1 插队尾”的操作,双端队列里的节点永远是按照距离单调递增排列的(比如队列里可能是:[距离5, 距离5, 距离6, 距离6, 距离7...])。

这样,我们每次从队首取出的节点,一定就是当前距离最小的节点,完美替代了最小堆的功能!

6.4 0-1 BFS 的代码模板 (Python)

from collections import deque

def zero_one_bfs(graph, start, n):
    # graph[u] = [(v, w), ...] 表示从 u 到 v 的边权为 w (w 只能是 0 或 1)
    INF = float('inf')
    dist = [INF] * n
    dist[start] = 0
    
    dq = deque()
    dq.append(start)
    
    while dq:
        u = dq.popleft() # 始终从队首取距离最小的节点
        
        for v, w in graph[u]:
            # 标准的松弛操作
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                # 核心区别在这里:
                if w == 0:
                    dq.appendleft(v) # 权值为0,插队首(距离不变,优先处理)
                else:
                    dq.append(v)     # 权值为1,插队尾(距离+1,往后排)
                    
    return dist

总结一下

  • 常规 BFS:大家步长一样,普通队列排队即可。
  • Dijkstra (最小堆):步长随意,必须用堆来随时挑出走得最近的人。
  • 0-1 BFS:步长只有 0 或 1。走 0 步的人直接插队到最前面,走 1 步的人乖乖去队尾。用双端队列就能完美维持顺序,比用最小堆更快。

参考资料:
https://blog.csdn.net/weixin_41806489/article/details/126852955
https://www.bilibili.com/video/BV1zz4y1m7Nq
https://www.bilibili.com/video/BV18a4y1A7gv
https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247488007&idx=1&sn=9d0dcfdf475168d26a5a4bd6fcd3505d&chksm=fd9cb918caeb300e1c8844583db5c5318a89e60d8d552747ff8c2256910d32acd9013c93058f&token=754098973&lang=zh_CN#rd

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

AI专题精讲

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

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

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

打赏作者

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

抵扣说明:

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

余额充值