1. 网格图BFS算法概述
网格图BFS(广度优先搜索)是解决二维矩阵中最短路径问题的经典算法。与树形结构的BFS不同,网格图BFS需要在二维平面上处理四个或八个方向的移动,同时要处理边界条件、障碍物等复杂情况。
提示:BFS之所以能保证找到最短路径,是因为它按照"涟漪扩散"的方式逐层遍历,第一次到达目标点时经过的路径必然是最短的。
在实际应用中,网格图BFS主要解决两类问题:
- 单源最短路径:从单个起点出发,寻找到特定目标点的最短路径
- 多源最短路径:从多个起点同时出发,计算每个位置到最近起点的距离
2. C语言实现基础模板
2.1 核心数据结构
在C语言中实现网格图BFS,首先需要定义几个关键数据结构:
c复制#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// 方向数组:左、右、上、下
const int DIRS[4][2] = {{0, -1}, {0, 1}, {-1, 0}, {1, 0}};
// 坐标结构体,模拟C++的pair<int, int>
typedef struct {
int x;
int y;
} Point;
方向数组DIRS定义了四个基本移动方向,如果需要八方向移动(包括对角线),可以扩展为:
c复制const int DIRS[8][2] = {
{-1,-1}, {-1,0}, {-1,1},
{0,-1}, {0,1},
{1,-1}, {1,0}, {1,1}
};
2.2 单源BFS完整实现
以下是标准的单源BFS实现模板:
c复制/**
* 网格图单源BFS函数
* @param grid: 二维字符矩阵
* @param m: 行数
* @param n: 列数
* @param start_x, start_y: 起点坐标
* @return: 返回动态分配的二维距离数组
*/
int** bfsGrid(char** grid, int m, int n, int start_x, int start_y) {
// 1. 初始化距离矩阵
int** dis = (int**)malloc(sizeof(int*) * m);
for (int i = 0; i < m; i++) {
dis[i] = (int*)malloc(sizeof(int) * n);
for (int j = 0; j < n; j++) {
dis[i][j] = -1; // -1表示未访问
}
}
// 2. 创建队列(数组模拟)
Point* queue = (Point*)malloc(sizeof(Point) * m * n);
int head = 0, tail = 0;
// 3. 起点入队
dis[start_x][start_y] = 0;
queue[tail++] = (Point){start_x, start_y};
// 4. BFS主循环
while (head < tail) {
Point curr = queue[head++];
// 遍历四个方向
for (int k = 0; k < 4; k++) {
int x = curr.x + DIRS[k][0];
int y = curr.y + DIRS[k][1];
// 边界检查+障碍检查+未访问检查
if (x >= 0 && x < m && y >= 0 && y < n &&
grid[x][y] == '.' && dis[x][y] < 0) {
dis[x][y] = dis[curr.x][curr.y] + 1;
queue[tail++] = (Point){x, y};
}
}
}
free(queue);
return dis;
}
2.3 关键实现细节解析
队列管理技巧
在C语言中,我们使用数组模拟队列,通过head和tail指针实现:
queue[tail++]:入队操作,尾部后移queue[head++]:出队操作,头部后移head < tail:队列非空条件
注意:网格BFS中,队列最大长度不会超过网格总大小(m×n),因此预先分配m×n的空间是安全的。
距离数组的双重作用
dis数组同时承担两个职责:
- 记录每个位置到起点的最短距离
- 作为访问标记(-1表示未访问)
这种设计既节省了内存,又简化了代码逻辑。
边界检查的完整写法
边界检查需要同时满足四个条件:
c复制x >= 0 && x < m && y >= 0 && y < n
这是确保坐标不越界的关键保障。
3. 经典问题实战解析
3.1 迷宫最近出口问题(LeetCode 1926)
问题描述
给定一个m×n的迷宫,其中:
- '.'表示空地
- '+'表示墙
- 入口是特定的(start_x, start_y)
- 出口是位于迷宫边界的任意空地
要求找到从入口到最近出口的最短路径长度。
解题思路
- 将入口作为BFS起点
- 每次扩展时检查是否到达边界
- 边界点且不是入口即为出口
完整实现
c复制int nearestExit(char** maze, int mazeSize, int* mazeColSize, int* entrance, int entranceSize) {
int m = mazeSize, n = mazeColSize[0];
int startR = entrance[0], startC = entrance[1];
typedef struct { int r, c, step; } Node;
Node* queue = (Node*)malloc(sizeof(Node) * m * n);
int head = 0, tail = 0;
// 入口入队并标记
queue[tail++] = (Node){startR, startC, 0};
maze[startR][startC] = '+';
while (head < tail) {
Node curr = queue[head++];
for (int k = 0; k < 4; k++) {
int nr = curr.r + DIRS[k][0];
int nc = curr.c + DIRS[k][1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && maze[nr][nc] == '.') {
// 检查是否为出口
if (nr == 0 || nr == m-1 || nc == 0 || nc == n-1) {
free(queue);
return curr.step + 1;
}
maze[nr][nc] = '+';
queue[tail++] = (Node){nr, nc, curr.step + 1};
}
}
}
free(queue);
return -1;
}
注意事项
- 原地修改迷宫标记可以节省vis数组空间
- 出口检查必须在边界检查之后进行
- 步数计算是当前步数+1(因为出口在下一位置)
3.2 二进制矩阵最短路径(LeetCode 1091)
问题特点
- 允许8方向移动
- 路径必须全为0
- 计算的是单元格数量(起点+途经点)
关键实现
c复制int shortestPathBinaryMatrix(int** grid, int gridSize, int* gridColSize) {
int n = gridSize;
if (grid[0][0] == 1 || grid[n-1][n-1] == 1) return -1;
if (n == 1) return 1;
typedef struct { int r, c, dist; } Node;
Node* queue = (Node*)malloc(sizeof(Node) * n * n);
int head = 0, tail = 0;
queue[tail++] = (Node){0, 0, 1};
grid[0][0] = 1;
while (head < tail) {
Node curr = queue[head++];
for (int k = 0; k < 8; k++) {
int nr = curr.r + DIRS8[k][0];
int nc = curr.c + DIRS8[k][1];
if (nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 0) {
if (nr == n-1 && nc == n-1) {
free(queue);
return curr.dist + 1;
}
grid[nr][nc] = 1;
queue[tail++] = (Node){nr, nc, curr.dist + 1};
}
}
}
free(queue);
return -1;
}
优化技巧
- 提前处理特殊case(单单元格情况)
- 8方向移动需要更大的DIRS数组
- 距离计算包含起点,所以初始dist=1
4. 多源BFS高级应用
4.1 多源BFS核心思想
多源BFS与单源的主要区别在于初始队列包含多个起点。想象这些起点同时向外扩散,就像多个石头同时投入水中产生的波纹相互干涉。
标准模板
c复制int multiSourceBFS(int** grid, int m, int n) {
Node* queue = (Node*)malloc(sizeof(Node) * m * n);
int head = 0, tail = 0;
// 将所有源点入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) { // 假设1是源点
queue[tail++] = (Node){i, j};
grid[i][j] = 2; // 标记已访问
}
}
}
int step = 0;
while (head < tail) {
int levelSize = tail - head; // 当前层节点数
for (int i = 0; i < levelSize; i++) {
Node curr = queue[head++];
for (int k = 0; k < 4; k++) {
int nr = curr.r + DIRS[k][0];
int nc = curr.c + DIRS[k][1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 0) {
grid[nr][nc] = 2;
queue[tail++] = (Node){nr, nc};
}
}
}
if (head < tail) step++; // 有效扩散才计数
}
free(queue);
return step;
}
4.2 典型应用场景
地图分析(LeetCode 1162)
计算每个海洋格子到最近陆地的最大距离。
c复制int maxDistance(int** grid, int gridSize, int* gridColSize) {
int n = gridSize;
Node* queue = (Node*)malloc(sizeof(Node) * n * n);
int head = 0, tail = 0;
// 所有陆地入队
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1) {
queue[tail++] = (Node){i, j};
}
}
}
if (tail == 0 || tail == n*n) return -1; // 全海洋或全陆地
int dist = -1;
while (head < tail) {
int levelSize = tail - head;
dist++;
for (int i = 0; i < levelSize; i++) {
Node curr = queue[head++];
for (int k = 0; k < 4; k++) {
int nr = curr.r + DIRS[k][0];
int nc = curr.c + DIRS[k][1];
if (nr >= 0 && nr < n && nc >= 0 && nc < n && grid[nr][nc] == 0) {
grid[nr][nc] = 1;
queue[tail++] = (Node){nr, nc};
}
}
}
}
free(queue);
return dist;
}
01矩阵(LeetCode 542)
计算每个1到最近0的距离。
c复制int** updateMatrix(int** mat, int matSize, int* matColSize, int* returnSize, int** returnColumnSizes) {
int m = matSize, n = matColSize[0];
int** dist = (int**)malloc(sizeof(int*) * m);
*returnColumnSizes = (int*)malloc(sizeof(int) * m);
Node* queue = (Node*)malloc(sizeof(Node) * m * n);
int head = 0, tail = 0;
for (int i = 0; i < m; i++) {
dist[i] = (int*)malloc(sizeof(int) * n);
(*returnColumnSizes)[i] = n;
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) {
dist[i][j] = 0;
queue[tail++] = (Node){i, j};
} else {
dist[i][j] = -1;
}
}
}
while (head < tail) {
Node curr = queue[head++];
for (int k = 0; k < 4; k++) {
int nr = curr.r + DIRS[k][0];
int nc = curr.c + DIRS[k][1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && dist[nr][nc] == -1) {
dist[nr][nc] = dist[curr.r][curr.c] + 1;
queue[tail++] = (Node){nr, nc};
}
}
}
free(queue);
*returnSize = m;
return dist;
}
4.3 腐烂橘子问题(LeetCode 994)
问题特点
- 多源点同时扩散
- 需要统计感染所有新鲜橘子的时间
- 需要处理无法全部感染的情况
完整实现
c复制int orangesRotting(int** grid, int gridSize, int* gridColSize) {
int m = gridSize, n = gridColSize[0];
int fresh = 0;
Node* queue = (Node*)malloc(sizeof(Node) * m * n);
int head = 0, tail = 0;
// 统计新鲜橘子并初始化队列
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 2) {
queue[tail++] = (Node){i, j};
} else if (grid[i][j] == 1) {
fresh++;
}
}
}
if (fresh == 0) return 0;
int minutes = 0;
while (head < tail && fresh > 0) {
int levelSize = tail - head;
minutes++;
for (int i = 0; i < levelSize; i++) {
Node curr = queue[head++];
for (int k = 0; k < 4; k++) {
int nr = curr.r + DIRS[k][0];
int nc = curr.c + DIRS[k][1];
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == 1) {
grid[nr][nc] = 2;
fresh--;
queue[tail++] = (Node){nr, nc};
}
}
}
}
free(queue);
return fresh == 0 ? minutes : -1;
}
关键点
- 使用fresh变量跟踪剩余新鲜橘子数量
- levelSize确保正确计时
- 提前终止条件(fresh > 0)优化性能
5. 性能优化与常见问题
5.1 内存管理最佳实践
- 二维数组分配遵循先行后列原则:
c复制int** arr = (int**)malloc(m * sizeof(int*));
for (int i = 0; i < m; i++) {
arr[i] = (int*)malloc(n * sizeof(int));
}
- 释放时反向操作:
c复制for (int i = 0; i < m; i++) free(arr[i]);
free(arr);
5.2 常见错误排查
- 队列溢出:确保队列大小足够(通常m×n)
- 访问越界:严格检查边界条件
- 标记时机:入队时立即标记,避免重复入队
- 步数计算:分清移动次数和路径长度
5.3 复杂度分析
- 时间复杂度:O(m×n),每个节点最多入队一次
- 空间复杂度:O(m×n),用于队列和访问标记
6. 扩展与变种
6.1 双向BFS
当起点和终点都已知时,可以从两端同时进行BFS,相遇时即得最短路径。适用于状态空间较大的情况。
6.2 优先队列BFS
当边权不相等时,使用优先队列(类似Dijkstra算法)代替普通队列。
6.3 三维BFS
将方向数组扩展到6个方向(上下左右前后),处理三维网格问题。
在实际刷题中,网格图BFS是必须掌握的基础算法。通过反复练习这些模板和例题,可以培养出快速识别问题类型并套用合适解法的能力。建议从单源BFS开始,逐步过渡到多源BFS,最后尝试更复杂的变种问题。