当前位置: 首页 > news >正文

【数据结构与算法】从广度优先搜索到Dijkstra算法解决单源最短路问题

前置知识

在学习最短路算法之前 , 要先掌握以下的图论基础概念来帮助理解后续内容 .

边和权

  • : 连接两个节点的连线 , 表示它们之间可达 .
  • 边权 : 每条边的非负数值 , 表示代价或距离 .
    • 无权图:所有边权相同 , 此时最短路径关注步数最少 .
    • 带权图:边权不一 , 要比较累积代价 .

稠密图和稀疏图

  • 稠密图 : 边数 E 接近最大值 V^2 , 适合使用邻接矩阵和 O(V^2) 算法 .
  • 稀疏图 : 边数 E 远小于 V^2 , 适合使用邻接表结合堆优化的 Dijkstra 算法达到 O((V+E)log⁡V) .

邻接矩阵和邻接表

  • 邻接矩阵

用二维数组 graph[i][j] 存储节点 i 到 j 的边权 :

   0    1    2    3
0 [0,   5, INF,   2]
1 [5,   0,   4, INF]
2 [INF, 4,   0,   1]
3 [2,  INF,  1,   0]
// INF 表示当前位置不可达
  • 优点 : 常数级查询复杂度 , 直观易理解 .
  • 缺点 : 空间复杂度 O(V²) , 当图为稀疏图时浪费严重 .
  • 邻接表

用列表存储每个节点的出边集合 :

List<Edge>[] adj = new List[V];
adj[0] = [(1,5), (3,2)];
adj[1] = [(0,5), (2,4)];
// …
  • 优点 : 空间 O(V+E) , 适合稀疏图 .
  • 缺点 : 查边需遍历链表 .

将邻接表恢复成邻接矩阵

  1. 创建 n×n 矩阵,初始化为 INF,对角线设为 0 .
  2. 遍历每个节点 u 的邻接表 , 对边 (v, w) 写入 mat[u][v] = w ;
static final int INF = Integer.MAX_VALUE / 2;
static class Edge {int to, weight;Edge(int to, int weight) { this.to = to; this.weight = weight; }
}public static int[][] listToMatrix(List<Edge>[] adj) {int n = adj.length;int[][] mat = new int[n][n];for(int i = 0; i < n; ++i){Arrays.fill(mat[i], INF);mat[i][i] = 0;}for(int u = 0; u < n; ++u){for(Edge e : adj[u]){mat[u][e.to] = e.weight;}}return mat;
}

广度优先搜索 BFS

BFS 按加入队列的层数进行矩阵或图的遍历 , 先探索所有距离为 1 的节点 , 再扩散探索距离为 2 的 , 以此类推 . 天然能求出无权图的最短步数 .

1091. 二进制矩阵中的最短路径

题面要求得到从矩阵左上角到矩阵右下角的经过的最短路径 , 矩阵中有不可达地块 . 如果用例无法到达右下角则返回 -1 .

  1. 按照广度优先搜索策略 , 我们首先初始化队列 , 将初始坐标入队 .
  2. 设置队列非空循环 , 每次入队维护路径的最大值 , 在这里将标记已访问的位置表示成广搜经过该位置时经过的路径数量 .
  3. 向八向拓展 , 判断拓展坐标是否在矩阵内以及是否可达 , 如果是则入队 , 并将该拓展坐标设置为已访问 .
  4. 最后如果队列为空说明无法达到右下角 , 如果检测到出队坐标位置是右下角则立刻返回该位置元素 .
class Solution {public int shortestPathBinaryMatrix(int[][] grid) {int[][] direction = {{1, 0}, {1, 1}, {1, -1}, {0, 1}, {0, -1}, {-1, 0}, {-1, 1}, {-1, -1}};Deque<int[]> dq = new ArrayDeque<>();int row = grid.length, col = grid[0].length;if(grid[0][0] != 0 || grid[row - 1][col - 1] != 0) return -1;dq.offerLast(new int[]{0, 0});grid[0][0] = 1;while(!dq.isEmpty()){int[] arr = dq.pollFirst();int a = arr[0], b = arr[1];if(a == row - 1 && b == col - 1){return grid[a][b];}for(int[] temp : direction){int r = a + temp[0], c = b + temp[1];if(r >= 0 && r < row && c >= 0 && c < col && grid[r][c] == 0){grid[r][c] = grid[a][b] + 1;dq.offerLast(new int[]{r, c});}}}return -1;}
}

BFS 的局限性

  • 仅适用于无权图 : 假设每条边代价相同 , 才能保证第一次到达即最短 .
  • 处理带权图无效 : 无法比较不同路径的累积权重 .

Dijkstra 算法

Dijkstra 是贪心的单源最短路径算法 , 适用于边权非负的带权图 .

对于 1091 的最短路问题 , 如果可达的地块 0 改成权 , 权表示到达该地块所花费的时间 , 最后求得从左上角到右下角的最小花费 , 则要这样改良 :

class Solution {public int shortestPathBinaryMatrix(int[][] grid) {int[][] direction = {{1, 0},{1, 1},{1, -1},{0, 1},{0, -1},{-1, 0},{-1, 1},{-1, -1}};int row = grid.length, col = grid[0].length;final int INF = Integer.MAX_VALUE;int[][] dist = new int[row][col];for(int i = 0; i < row; ++i){Arrays.fill(dist[i], INF);}dist[0][0] = grid[0][0];PriorityQueue<int[]> pq = new PriorityQueue<>((a,b) -> a[0] - b[0]);pq.offer(new int[]{dist[0][0], 0, 0});while(!pq.isEmpty()){int[] cur = pq.poll();int cost = cur[0], x = cur[1], y = cur[2];if(cost > dist[x][y]) continue;if(x == row - 1 && y == col - 1){return cost;}for(int[] d : direction){int nx = x + d[0], ny = y + d[1];if(nx < 0 || nx >= row || ny < 0 || ny >= col) continue;int nc = cost + grid[nx][ny];if(nc < dist[nx][ny]){dist[nx][ny] = nc;pq.offer(new int[]{nc, nx, ny});}}}return -1;}
}

因为矩阵带权 , 更近的路径可能花费更多 , 所以 BFS 队列的先进后出策略是行不通的 , 在这里使用小顶堆的优先队列来让已探索路径中花费最小的位置出队 , 同时拓展出队的坐标 , 维护表示从起点到该位置的最小花费的 cost 数组 .

如果不标记已访问的位置 , 如何保证搜索不成环 ?

答案是 if(cost > dist[x][y]) continue; , 如果这条路径花费更高 , 则放弃这条路径 . cost 数组是不关心具体路径的 , 我们拒绝拓展花费更高的路径 , 保证了每条路径最多探索一次 , 同时因为每一步都要花费 , 所以不会出现某一条路径重复走之前的地块形成环的情况 . 这也是为什么 Dijkstra 只能解决非负权图的原因 , 如果权有负 , 那么贪心的策略会使搜索 “回去” .

所以 Dijkstra 算法其实是一种最小花费优先的广度优先搜索策略 .

对比 BFS 与 Dijkstra

特性BFSDijkstra
适用图无权图边权非负的带权图
核心结构队列最小堆
距离定义步数累积权重
时间复杂度O(V+E)邻接矩阵 : O(V^2) ; 堆优化 : O((V+E)log⁡V)

Dijkstra 不是简单的先进先出关系 , 而是在已探索的地块中选择最小花费的来向四周拓展 , 这是使用小顶堆优先队列实现的 .

在拓展中维护表示到达当前地块的最小花费的 cost 数组 , 保证从优先队列出队时 , 出队坐标的 cost 一定是从起点发展的最小花费 .

相关文章:

  • Linux权限探秘:驾驭权限模型,筑牢系统安全
  • 主流嵌入式Shell工具性能对比
  • 视频音频去掉开头结尾 视频去掉前n秒后n秒 电视剧去掉开头歌曲
  • 2025-04-22-X86 架构与 Arm 架构异同及应用
  • 【LeetCode】算法详解#6 ---除自身以外数组的乘积
  • python之可视化图形生成
  • AI短视频创富营
  • MCP(Model Context Protocol)与提示词撰写
  • 打卡第48天
  • 基于 llama-factory进行模型微调
  • android 模拟器如何进行单模块更新
  • SpringSecurity+vue通用权限系统2
  • 【设计模式】2.策略模式
  • Python Selenium登录网易邮箱
  • springboot启动mapper找不到方法对应的xml
  • 分形几何在医学可视化中的应用:从理论到Python实战
  • 支持selenium的chrome driver更新到137.0.7151.68
  • 【CSS-8】深入理解CSS选择器权重:掌握样式优先级的关键
  • LLMs 系列科普文(11)
  • U盘安装ubuntu系统
  • 软件开发平台搭建/泉州seo报价
  • 企业网站制作报价/百度搜索广告
  • wordpress 删除gravatar/余姚关键词优化公司
  • 网站建设发布ps科技感/网络推广公司官网
  • 网站模板 扁平化/淘宝运营培训课程
  • 免费用手机建立网站/常见的网络营销方法有哪些