BFS在路径搜索中的应用
目录
一、LeetCode 675.为高尔夫比赛砍树:BFS解决多节点路径累加
二、LeetCode 127.单词接龙:BFS解决“字符串变换最短路径”
三、LeetCode 433.最小基因变化:BFS解决“基因序列最短突变”
四、LeetCode 1926.迷宫中离入口最近的出口:BFS解决“迷宫最短出口”
五、总结:BFS的通用框架与场景适配
在算法题中,广度优先搜索(BFS)是解决“最短路径”“可达性判断”类问题的核心方法之一。本文将以LeetCode 127.单词接龙、433.最小基因变化、675.为高尔夫比赛砍树、1926.迷宫中离入口最近的出口四题为例,结合代码优化(保留原变量名,仅修正逻辑与补充功能),拆解BFS在不同场景下的应用逻辑。
一、LeetCode 675.为高尔夫比赛砍树:BFS解决多节点路径累加
原题要求“按树的高度升序砍树,从(0,0)出发,计算总步数,无法砍完则返回-1”。最初代码的 divcount 函数仅做递归遍历,未计算步数,优化后用BFS实现路径长度计算,同时保留原变量名。
问题拆解
- 输入:二维网格 forest (0=障碍,1=空地,>1=树,树的数值=高度)。
- 规则:必须按“树高升序”砍树,从(0,0)出发,只能走空地/树(砍树后变为空地)。
- 目标:求砍完所有树的总步数(若某棵树不可达返回-1)。
核心思路
问题拆解为“多段最短路径累加”:先按树高排序,再用BFS依次计算“当前位置到下一棵需砍树的最短路径”,总步数为各段路径之和。
步骤落地
1. 预处理:收集并排序树
- 遍历 forest ,收集所有“树”的信息(高度,坐标编码),存入列表 stemp (坐标编码=行×
列数+列,便于后续解码)。
- 对 stemp 按“树高升序”排序,确定砍树顺序。
2. 初始化:起点与总步数
- 起点 (sx, sy) 初始化为(0,0)(初始位置)。
- 总步数 totalSteps 初始化为0。
3. BFS计算每段路
遍历排序后的每棵树:
- 解码树的坐标 (row, col) (行=编码÷列数,列=编码%列数),跳过空地(值=1)和障碍(值=0)。
- 调用BFS函数,计算从 (sx, sy) 到 (row, col) 的最短步数:
- BFS内:队列存当前位置, visited 标记已访问,按“上/下/左/右”四个方向遍历,找到目标位置返回步数,否则返回-1。
- 若某段路径返回-1(树不可达),直接返回-1;否则将步数累加到 totalSteps ,并更新起点为当前树的位置 (row, col) 。
4. 返回结果
遍历完所有树,返回 totalSteps
代码
class Solution {
public:// 用BFS计算从(sx,sy)到(x,y)的最短步数,保留原函数名与参数int divcount(int sx, int sy, int x, int y, vector<vector<int>>& forest) {if (sx == x && sy == y) return 0; // 起点即终点,步数为0int m = forest.size(), n = forest[0].size();vector<vector<bool>> visited(m, vector<bool>(n, false)); // 新增:标记已访问节点,避免重复queue<pair<int, int>> q;q.push({sx, sy});visited[sx][sy] = true;int steps = 0;vector<pair<int, int>> dirs = {{-1,0},{1,0},{0,-1},{0,1}}; // 新增:四个方向while (!q.empty()) {int size = q.size();steps++; // 每一层对应一步for (int i = 0; i < size; i++) {auto curr = q.front();q.pop();// 遍历四个方向for (auto& dir : dirs) {int nx = curr.first + dir.first;int ny = curr.second + dir.second;// 边界+非障碍+未访问判断if (nx >= 0 && nx < m && ny >=0 && ny < n && forest[nx][ny] != 0 && !visited[nx][ny]) {if (nx == x && ny == y) return steps; // 到达目标,返回当前步数visited[nx][ny] = true;q.push({nx, ny});}}}}return -1; // 无法到达}int cutOffTree(vector<vector<int>>& forest) {vector<pair<int, int>> stemp; // 保留原变量:存储(树高, 坐标编码)int sx = 0, sy = 0; // 保留原变量:当前起点int n = forest[0].size(); // 保留原变量:列数int totalSteps = 0; // 新增:总步数累加// 逻辑:收集所有节点信息for (int i = 0; i < forest.size(); i++) {for (int j = 0; j < forest[0].size(); j++) {stemp.push_back({forest[i][j], i * forest[0].size() + j});}}// 逻辑:按树高升序排序sort(stemp.begin(), stemp.end(),[](const pair<int, int>& a, const pair<int, int>& b) {return a.first < b.first;});// 遍历每棵树,计算路径并累加for (const auto& p : stemp) {int val = p.first;int idx = p.second;int row = idx / n; // 解码行坐标int col = idx % n; // 解码列坐标if (val == 0 || val == 1) continue; // 跳过障碍和空地int steps = divcount(sx, sy, row, col, forest);if (steps == -1) return -1; // 某棵树无法到达,直接返回-1totalSteps += steps;sx = row; // 更新起点为当前树的位置sy = col;}return totalSteps;}
};
核心思路
1. BFS的必要性:递归(DFS)会陷入“深度优先”,无法保证最短路径,而BFS按“层”遍历,第一层即最短步数。
2. 变量复用:保留 stemp (存储树信息)、 sx/sy (当前起点)、 row/col (目标坐标)等原变量,仅新增 visited (避免重复访问)、 dirs (方向数组)、 totalSteps (总步数)。
二、LeetCode 127.单词接龙:BFS解决“字符串变换最短路径”
原题要求“从beginWord到endWord,每次变一个字符,仅用wordList中的词,求最短变换次数”。核心是将“单词”视为“节点”,“一次合法变换”视为“边”,用BFS找最短路径。
问题拆解
- 输入:起点单词 beginWord 、终点单词 endWord 、合法单词列表 wordList 。
- 规则:每次仅修改1个字符,新单词必须在 wordList 中,且不重复使用。
- 目标:求从 beginWord 到 endWord 的最短变换步数(若不可达返回0)。
核心思路
将“单词”视为“节点”,“单次合法字符修改”视为“边”,问题转化为“无向图中起点到终点的最短路径”,用BFS实现层遍历(每一层对应一步变换)。
步骤落地
1. 预处理:快速判断合法性
将 wordList 转为哈希集合 wordSet ,实现O(1)查询(判断修改后的单词是否合法),同时检查 endWord 是否在 wordSet 中,若不在直接返回0。
2. BFS初始化
- 队列 q 存入起点 beginWord ,记录当前层的单词。
- 哈希集合 visited 存入 beginWord ,避免单词重复入队(防止“hot→dot→hot”的循环)。
- 步数 step 初始化为1(起点本身算第一步)。
3. 层遍历求最短路径
- 每次取队列当前大小 size ,确定“当前步的所有单词”。
- 遍历每个单词:对其每个字符,尝试替换为 a-z 中除原字符外的所有字符,生成新单词。
- 若新单词等于 endWord ,直接返回 step + 1 (当前替换为新一步)。
- 若新单词在 wordSet 且未被 visited 标记,加入队列并标记已访问。
- 一层遍历结束后, step += 1 (进入下一步)。
4. 终止判断
队列空仍未找到 endWord ,返回0(不可达)。
代码
class Solution {
public:int ladderLength(string beginWord, string endWord, vector<string>& wordList) {unordered_set<string> wordSet(wordList.begin(), wordList.end()); // 快速判断单词是否存在if (wordSet.find(endWord) == wordSet.end()) return 0; // 终点不在列表中,直接返回0queue<string> q; // 存储当前层的单词q.push(beginWord);int step = 1; // 新增:初始步数(起点算第一步)unordered_set<string> visited; // 标记已访问单词,避免循环visited.insert(beginWord);while (!q.empty()) {int size = q.size();step++; // 每一层对应一步变换for (int i = 0; i < size; i++) {string curr = q.front();q.pop();// 遍历当前单词的每个字符,尝试替换为a-zfor (int j = 0; j < curr.size(); j++) {char original = curr[j]; // 保留原字符,后续恢复for (char c = 'a'; c <= 'z'; c++) {if (c == original) continue; // 跳过原字符curr[j] = c;// 到达终点,返回当前步数if (curr == endWord) return step;// 单词存在且未访问,加入队列if (wordSet.count(curr) && !visited.count(curr)) {visited.insert(curr);q.push(curr);}}curr[j] = original; // 恢复原字符,尝试下一个位置}}}return 0; // 无法到达终点}
};
三、LeetCode 433.最小基因变化:BFS解决“基因序列最短突变”
原题与“单词接龙”逻辑高度相似:将“基因序列”视为“节点”,“单个碱基替换(A/T/C/G)”视为“边”,求从start到end的最短突变次数。
问题拆解
- 输入:起点基因 start 、终点基因 end 、合法基因列表 bank 。
- 规则:基因由 A/T/C/G 组成(长度固定),每次仅替换1个碱基,新基因必须在 bank 中,且不重复使用。
- 目标:求从 start 到 end 的最短突变次数(若不可达返回-1)。
核心思路
与“单词接龙”逻辑同源:将“基因序列”视为“节点”,“单次碱基替换”视为“边”,BFS层遍历求最短路径(每一层对应一次突变)。
步骤落地
1. 预处理:合法性校验
将 bank 转为哈希集合 bankSet ,检查 end 是否在 bankSet 中,若不在直接返回-1。
2. BFS初始化
- 队列 q 存入起点 start ,记录当前层的基因。
- 哈希集合 visited 存入 start ,避免重复突变。
- 步数 step 初始化为0(起点算“未突变”状态,第一次突变后step+1)。
3. 层遍历求最短突变
- 取队列当前大小 size ,遍历当前层每个基因。
- 对基因的每个位置,尝试替换为 A/T/C/G 中除原碱基外的3种碱基,生成新基因:
- 若新基因等于 end ,返回 step + 1 (当前替换为一次突变)。
- 若新基因在 bankSet 且未被标记,加入队列并标记已访问。
- 一层遍历结束后, step += 1 (进入下一次突变)。
4. 终止判断
队列空仍未找到 end ,返回-1(不可达)。
代码
class Solution {
public:int minMutation(string start, string end, vector<string>& bank) {unordered_set<string> bankSet(bank.begin(), bank.end()); //存储合法基因if (bankSet.find(end) == bankSet.end()) return -1; // 终点不合法,返回-1queue<string> q; // 存储当前层基因q.push(start);int step = 0; // 突变次数(起点算0次)unordered_set<string> visited; // 避免重复访问visited.insert(start);vector<char> genes = {'A', 'T', 'C', 'G'}; // 合法碱基while (!q.empty()) {int size = q.size();for (int i = 0; i < size; i++) {string curr = q.front();q.pop();if (curr == end) return step; // 到达终点,返回当前次数// 遍历每个位置,尝试替换为4种碱基for (int j = 0; j < curr.size(); j++) {char original = curr[j];for (char g : genes) {if (g == original) continue;curr[j] = g;// 基因合法且未访问,加入队列if (bankSet.count(curr) && !visited.count(curr)) {visited.insert(curr);q.push(curr);}}curr[j] = original; // 恢复原碱基}}step++; // 每一层结束,突变次数+1}return -1; // 无法到达}
};
与单词接龙的共性
1. 节点与边的定义一致:均为“字符串+单个字符替换”。
2. BFS框架复用:均通过“队列存层→遍历层→生成下一层节点”实现最短路径。
四、LeetCode 1926.迷宫中离入口最近的出口:BFS解决“迷宫最短出口”
原题要求“从入口出发,找到离入口最近的出口(边界格子),仅能走空地('.'),墙('+')不可走”。核心是“边界判断”+“BFS最短路径”。
问题拆解
- 输入:二维迷宫 maze ( . =空地, + =墙)、入口坐标 entrance (行,列)。
- 规则:仅能走空地,不可穿墙,不可重复走同一格子。
- 目标:求从入口到“出口”(迷宫边界的空地)的最短步数(若不可达返回-1)。
核心思路
将“迷宫格子”视为“节点”,“相邻空地(上/下/左/右)”视为“边”,问题转化为“从入口到边界空地的最短路径”,BFS层遍历(每一层对应一步移动),同时用“修改原迷宫”标记已访问(节省空间)。
步骤落地
1. BFS初始化
- 队列 q 存入入口坐标 (entrance[0], entrance[1]) ,记录当前层的位置。
- 将入口格子改为 + (标记为墙,避免重复访问,无需额外 visited 数组)。
- 步数 step 初始化为0。
2. 层遍历找最近出口
- 每次取队列当前大小 size ,确定“当前步的所有位置”。
- 遍历每个位置:按“上/下/左/右”四个方向生成新位置 (nx, ny) :
- 先判断 (nx, ny) 是否在迷宫边界内(避免越界)。
- 若新位置是空地( . ):
- 检查是否为出口( nx 是0或迷宫行数-1,或 ny 是0或迷宫列数-1),若是直接返回 step + 1 。
- 将新位置改为 + (标记已访问),加入队列。
- 一层遍历结束后, step += 1 (进入下一步)。
3. 终止判断
队列空仍未找到出口,返回-1(不可达)。
代码
class Solution {
public:int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {int m = maze.size(), n = maze[0].size();queue<pair<int, int>> q; //当前位置(行,列)q.push({entrance[0], entrance[1]});maze[entrance[0]][entrance[1]] = '+'; // 标记入口为墙,避免重复访问int step = 0;vector<pair<int, int>> dirs = {{-1,0},{1,0},{0,-1},{0,1}}; // 新增:四个方向while (!q.empty()) {int size = q.size();step++; // 每一层对应一步for (int i = 0; i < size; i++) {auto curr = q.front();q.pop();int x = curr.first, y = curr.second;// 遍历四个方向for (auto& dir : dirs) {int nx = x + dir.first;int ny = y + dir.second;// 边界判断:是否在迷宫内if (nx >= 0 && nx < m && ny >= 0 && ny < n) {if (maze[nx][ny] == '.') {// 判断是否为出口(边界格子)if (nx == 0 || nx == m-1 || ny == 0 || ny == n-1) {return step;}maze[nx][ny] = '+'; // 标记为墙,避免重复q.push({nx, ny});}}}}}return -1; // 无出口}
};
五、总结:BFS的通用框架与场景适配
通过四题的代码优化与思路解析,可提炼出BFS解决“最短路径/可达性”问题的通用框架:
1. 初始化:队列存入起点,标记起点已访问(避免循环)。
2. 层遍历:
- 记录当前层大小(确定每一步的节点数)。
- 遍历当前层每个节点,生成所有合法的下一层节点(方向/字符替换)。
3. 终止条件:
- 找到目标节点(如675题的树、127题的endWord),返回当前步数。
- 队列为空仍未找到,返回-1(或0,视题目要求)。
不同场景的适配仅需修改“节点合法性判断”(如迷宫的墙、单词的字符替换)与“目标节点定义”(如出口、endWord),核心逻辑高度复用。掌握这一框架,可轻松应对各类BFS路径搜索问题。