【优选算法篇】BFS 解决最短路——寻找最优路径的真谛

文章目录

    • [一、 核心原理:为什么最短路首选 BFS?](#一、 核心原理:为什么最短路首选 BFS?)
    • [二、 迷宫中离入口最近的出口 (Medium)](#二、 迷宫中离入口最近的出口 (Medium))
      • [2.1 题目描述](#2.1 题目描述)
      • [2.2 深度拆解:层序遍历的步数控制](#2.2 深度拆解:层序遍历的步数控制)
      • [2.3 C++ 代码实战](#2.3 C++ 代码实战)
    • [三、 最小基因变化 (Medium)](#三、 最小基因变化 (Medium))
      • [3.1 题目描述](#3.1 题目描述)
      • [3.2 深度拆解:字符串作为"坐标"](#3.2 深度拆解:字符串作为“坐标”)
      • [3.3 C++ 代码实战](#3.3 C++ 代码实战)
    • [四、 单词接龙 (Hard)](#四、 单词接龙 (Hard))
      • [4.1 题目描述](#4.1 题目描述)
      • [4.2 深度拆解:为何是 Hard?](#4.2 深度拆解:为何是 Hard?)
      • [4.3 C++ 代码实战](#4.3 C++ 代码实战)
    • [五、 为高尔夫比赛砍树 (Hard)](#五、 为高尔夫比赛砍树 (Hard))
      • [5.1 题目描述](#5.1 题目描述)
      • [5.2 深度拆解:最短路的叠加](#5.2 深度拆解:最短路的叠加)
      • [5.3 C++ 代码实战](#5.3 C++ 代码实战)
    • [六、 总结与避坑](#六、 总结与避坑)

一、 核心原理:为什么最短路首选 BFS?

💬 底层逻辑

BFS(广度优先搜索)就像在水面上丢一颗石子,涟漪是均匀地、一圈一圈地向外扩散的。

  1. 等权性:在棋盘、迷宫或变换步数为 1 的图中,每一层扩散代表步数 +1。
  2. 最优性证明 :假设目标点在第 k k k 层。由于 BFS 严格按照层级搜索,在搜到第 k k k 层之前,所有的 1 , 2 , ... , k − 1 1, 2, \dots, k-1 1,2,...,k−1 层都已经搜过了且没找到目标。因此,第一次接触到目标点时,所经过的路径长度必定是最小的。

🚀 建模三步走

  • 确定状态(点) :坐标 (x, y)、基因序列 string、单词 string
  • 确定转移(边):上下左右走一步、变一个字符。
  • 确定边界:越界判断、是否在基因库/字典中、是否已访问(防止死循环)。

二、 迷宫中离入口最近的出口 (Medium)

2.1 题目描述

链接1926. 迷宫中离入口最近的出口

给定一个 m x n 的迷宫,. 是空地,+ 是墙。从 entrance 出发,找到离入口最近的边界空地(出口),返回最少步数。入口本身不算出口。

2.2 深度拆解:层序遍历的步数控制

  • 搜索起点:入口坐标。
  • 搜索终点 :坐标在第一行、最后一行、第一列或最后一列,且不是入口点。
  • 层序计数技巧
    while(!q.empty()) 内部,先获取 int sz = q.size(),一次性处理完这 sz 个节点。这 sz 个节点属于同一"层"(即步数相同)。这样,我们只需在处理完一层后 step++ 即可。

2.3 C++ 代码实战

cpp 复制代码
class Solution {
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};

public:
    int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {
        int m = maze.size(), n = maze[0].size();
        // 1. 记录访问状态,防止走回头路
        vector<vector<bool>> vis(m, vector<bool>(n, false));
        
        queue<pair<int, int>> q;
        q.push({entrance[0], entrance[1]});
        vis[entrance[0]][entrance[1]] = true;

        int step = 0;
        while (!q.empty()) {
            step++; // 准备扩散到下一层
            int sz = q.size(); // 锁定当前层节点数
            
            for (int i = 0; i < sz; i++) {
                auto [a, b] = q.front(); q.pop();

                for (int j = 0; j < 4; j++) {
                    int x = a + dx[j], y = b + dy[j];
                    
                    // 2. 检查:不越界 && 是通路 && 没走过
                    if (x >= 0 && x < m && y >= 0 && y < n && maze[x][y] == '.' && !vis[x][y]) {
                        // 3. 判断是否为出口:在边缘上
                        if (x == 0 || x == m - 1 || y == 0 || y == n - 1) return step;
                        
                        vis[x][y] = true;
                        q.push({x, y});
                    }
                }
            }
        }
        return -1; // 搜遍全图也出不去
    }
};

三、 最小基因变化 (Medium)

3.1 题目描述

链接433. 最小基因变化

8个字符组成的基因。一次变化改变一个字符。必须在 bank 中的变化才有效。求从 startend 的最小变化次数。

3.2 深度拆解:字符串作为"坐标"

这题的本质是在字符串组成的抽象图中找最短路

  • 状态转移 :对于当前基因的 8 个位置,每个位置尝试替换为 A, C, G, T 4 种。
  • 合法性判定 :变换后的字符串必须出现在 bank 集合中。
  • 性能优化 :将 bank 转化为 unordered_set,查询时间从 O ( N ) O(N) O(N) 降到 O ( 1 ) O(1) O(1)。

3.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int minMutation(string start, string end, vector<string>& bank) {
        unordered_set<string> dict(bank.begin(), bank.end());
        if (!dict.count(end)) return -1; // 终点不合法,直接判死刑

        unordered_set<string> vis; // 字符串版本的 vis
        queue<string> q;
        q.push(start);
        vis.insert(start);

        int step = 0;
        string choice = "ACGT";

        while (!q.empty()) {
            int sz = q.size();
            while (sz--) {
                string curr = q.front(); q.pop();
                if (curr == end) return step;

                // 核心:尝试改变 8 个位置中的每一个
                for (int i = 0; i < 8; i++) {
                    char oldChar = curr[i];
                    for (char ch : choice) {
                        curr[i] = ch; // 模拟变换
                        // 如果变换后的基因合法且未访问
                        if (dict.count(curr) && !vis.count(curr)) {
                            vis.insert(curr);
                            q.push(curr);
                        }
                    }
                    curr[i] = oldChar; // 状态恢复,准备尝试下一个位置
                }
            }
            step++;
        }
        return -1;
    }
};

四、 单词接龙 (Hard)

4.1 题目描述

链接127. 单词接龙

找到从 beginWordendWord 的最短转换序列中的 单词数目

4.2 深度拆解:为何是 Hard?

与"基因变化"逻辑一致,但难点在于:

  1. 返回值 :要求的是"单词数目",即 步数 + 1
  2. 数据规模wordList 可能很大。如果对每个单词去和 List 里的其他单词比(看是否只差一个字母),时间复杂度 O ( N 2 × L ) O(N^2 \times L) O(N2×L)。
  3. 优化策略 :依然采取 "改变当前单词的每一位" ,复杂度降为 O ( N × L × 26 ) O(N \times L \times 26) O(N×L×26),在 L L L 很小时效果极佳。

4.3 C++ 代码实战

cpp 复制代码
class Solution {
public:
    int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> dict(wordList.begin(), wordList.end());
        if (!dict.count(endWord)) return 0;

        queue<string> q;
        q.push(beginWord);
        // 初始单词也算一个,所以 step 从 1 开始
        int step = 1;

        while (!q.empty()) {
            int sz = q.size();
            while (sz--) {
                string word = q.front(); q.pop();
                if (word == endWord) return step;

                // 尝试修改每一位字母
                for (int i = 0; i < word.size(); i++) {
                    char original = word[i];
                    for (char c = 'a'; c <= 'z'; c++) {
                        word[i] = c;
                        if (dict.count(word)) {
                            q.push(word);
                            dict.erase(word); // 优化:erase 相当于标记已访问
                        }
                    }
                    word[i] = original;
                }
            }
            step++;
        }
        return 0;
    }
};

五、 为高尔夫比赛砍树 (Hard)

5.1 题目描述

链接675. 为高尔夫比赛砍树

必须按照树的高度从小到大顺序砍掉所有树。求总步数。

5.2 深度拆解:最短路的叠加

这题的 Hard 在于它是多次最短路求和

  1. 预处理 :扫描矩阵,提取所有树的坐标,按高度 sort

  2. 串联搜索

    • 第 1 阶段:从 (0, 0) 走到高度第 1 矮的树。
    • 第 2 阶段:从第 1 矮的树走到第 2 矮的树。
    • ...
  3. 结果累加:每一次移动都是一次独立的 BFS,如果中途任何一个 BFS 失败(返回 -1),整个任务直接失败。

5.3 C++ 代码实战

cpp 复制代码
class Solution {
    int m, n;
    int dx[4] = {0, 0, 1, -1};
    int dy[4] = {1, -1, 0, 0};

public:
    int cutOffTree(vector<vector<int>>& forest) {
        m = forest.size(); n = forest[0].size();
        vector<pair<int, int>> trees;
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (forest[i][j] > 1) trees.push_back({i, j});
            }
        }
        // 1. 按高度排序
        sort(trees.begin(), trees.end(), [&](pair<int,int>& a, pair<int,int>& b){
            return forest[a.first][a.second] < forest[b.first][b.second];
        });

        // 2. 依次计算每两棵树之间的最短距离
        int startX = 0, startY = 0;
        int totalSteps = 0;
        for (auto& tree : trees) {
            int d = bfs(forest, startX, startY, tree.first, tree.second);
            if (d == -1) return -1; // 无法到达
            totalSteps += d;
            startX = tree.first; startY = tree.second; // 砍完这棵,这就是新起点
        }
        return totalSteps;
    }

    int bfs(vector<vector<int>>& f, int sx, int sy, int tx, int ty) {
        if (sx == tx && sy == ty) return 0;
        queue<pair<int, int>> q;
        vector<vector<bool>> vis(m, vector<bool>(n, false));
        q.push({sx, sy});
        vis[sx][sy] = true;

        int step = 0;
        while (!q.empty()) {
            step++;
            int sz = q.size();
            while (sz--) {
                auto [a, b] = q.front(); q.pop();
                for (int i = 0; i < 4; i++) {
                    int x = a + dx[i], y = b + dy[i];
                    if (x >= 0 && x < m && y >= 0 && y < n && f[x][y] > 0 && !vis[x][y]) {
                        if (x == tx && y == ty) return step;
                        vis[x][y] = true;
                        q.push({x, y});
                    }
                }
            }
        }
        return -1;
    }
};

六、 总结与避坑

💬 复盘:BFS 最短路是模板化程度非常高的算法。

  1. 步数同步 :一定要用 sz = q.size() 锁定当前层,否则步数统计会乱。
  2. 提前返回 :在 push 下一层节点时直接判断是否到达目标,比弹出时再判断快一倍。
  3. 空间换时间unordered_setvis 数组是搜索的铁律。
相关推荐
雅俗共赏1002 小时前
医学图像重建中常用的迭代求解器分类
图像处理·算法
不熬夜的熬润之2 小时前
KCF算法解析
人工智能·算法·计算机视觉·机器人
CoderCodingNo2 小时前
【CSP】CSP-J 2025真题 | 多边形 luogu-P14360 (相当于GESP六级水平)
开发语言·c++·算法
凉城a2 小时前
前端预检请求是什么?
前端·面试
Magic--2 小时前
【LeetCode 27. 移除元素】C++ 范围 for 极简实现与原理解析
c++·算法·leetcode
FPGA小迷弟2 小时前
FPGA工程师面试题汇总(九)
网络协议·tcp/ip·fpga开发·面试·verilog·fpga
A923A2 小时前
【洛谷刷题 | 第九天】
算法·二分·洛谷
旖-旎2 小时前
位运算(只出现一次的数字|||)(5)
c++·算法·leetcode·位运算
AIpanda8882 小时前
数字员工是什么?熊猫智汇在提高销售转化率中的作用有哪些?
算法