广度优先搜索(BFS)是处理二维网格(矩阵)类问题的"标配"算法,核心逻辑是从起始点出发,按"层"向四周扩散,结合"方向数组"处理上下左右四个方向的邻接节点。它能高效解决"区域填充""连通域计数/面积计算""包围区域判定"等经典网格问题,是算法面试中网格类题目最常考的解法之一。本文通过4道经典区域填充题目,拆解BFS的核心解题框架与场景化应用技巧。
一、图像渲染(洪水填充)
题目描述:
给定一个二维整数数组 image 表示图像像素值,以及起始坐标 (sr, sc) 和目标颜色 color,执行"洪水填充":将起始坐标所在的连通区域(像素值与起始点相同)全部替换为目标颜色(连通区域定义为上下左右相邻的同值像素)。
示例:
- 输入:
image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, color = 2 - 输出:
[[2,2,2],[2,2,0],[2,0,1]]
解题思路:
标准BFS扩散框架 + 方向数组:
- 记录起始点的原始颜色
prev,若prev == color直接返回(无需填充)。 - 初始化队列,将起始坐标入队。
- 循环处理队列:
- 取出队首坐标,将其像素值改为目标颜色;
- 遍历四个方向,若邻接坐标在网格内且像素值为
prev,则入队。
- 遍历结束后返回修改后的图像。
完整代码:
cpp
class Solution {
int dx[4] = {0, 0, 1, -1}; // 上下左右方向数组(行偏移)
int dy[4] = {1, -1, 0, 0}; // 列偏移
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
int prev = image[sr][sc];
if(prev == color) return image; // 无需填充
queue<pair<int, int>> q;
q.push({sr, sc});
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
image[a][b] = color; // 填充当前点
// 遍历四个方向
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
// 边界检查 + 颜色匹配
if(x >= 0 && x < image.size() && y >= 0 && y < image[0].size() && image[x][y] == prev)
q.push({x, y});
}
}
return image;
}
};
复杂度分析:
- 时间复杂度:O(mn)O(mn)O(mn),
m/n为网格的行/列数,每个像素最多入队一次。 - 空间复杂度:O(mn)O(mn)O(mn),队列最坏情况存储所有需要填充的像素(如全图同色)。
二、岛屿数量
题目描述:
给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算岛屿的数量(岛屿由相邻的陆地连接而成,相邻指上下左右方向,网格边界外视为水)。
示例:
-
输入:
[["1","1","0","0","0"], ["1","1","0","0","0"], ["0","0","1","0","0"], ["0","0","0","1","1"]] -
输出:
3
解题思路:
BFS遍历 + 访问标记:
- 初始化访问标记数组
vis,记录已遍历的陆地坐标。 - 遍历网格的每个坐标:
- 若当前坐标是陆地且未访问,岛屿数
ret++,启动BFS遍历该岛屿的所有连通陆地;
- 若当前坐标是陆地且未访问,岛屿数
- BFS逻辑:
- 起始坐标入队并标记为已访问;
- 遍历四个方向,若邻接坐标是陆地且未访问,入队并标记。
完整代码:
cpp
class Solution {
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
bool vis[301][301] = {false}; // 访问标记
int m, n; // 网格行数、列数
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size(), n = grid[0].size();
int ret = 0;
// 遍历所有网格点
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
{
if(grid[i][j] == '1' && !vis[i][j])
{
ret++;
bfs(grid, i, j); // 遍历当前岛屿的所有连通陆地
}
}
return ret;
}
void bfs(vector<vector<char>>& grid, int i, int j)
{
queue<pair<int, int>> q;
q.push({i, j});
vis[i][j] = true; // 标记已访问
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
// 遍历四个方向
for(int k = 0; k < 4; k++)
{
int x = a + dx[k], y = b + dy[k];
// 边界检查 + 陆地 + 未访问
if(x >= 0 && y >= 0 && x < grid.size() && y < grid[0].size() && grid[x][y] == '1' && !vis[x][y])
{
vis[x][y] = true;
q.push({x, y});
}
}
}
}
};
复杂度分析:
- 时间复杂度:O(mn)O(mn)O(mn),每个网格点仅遍历一次。
- 空间复杂度:O(mn)O(mn)O(mn),访问标记数组 + 队列(最坏情况存储所有陆地坐标)。
三、岛屿的最大面积
题目描述:
给定一个由 0(水)和 1(陆地)组成的二维网格,返回最大的岛屿面积(岛屿定义同"岛屿数量")。
示例:
-
输入:
[[0,0,1,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,1,1,0,1,0,0,0,0,0,0,0,0], [0,1,0,0,1,1,0,0,1,0,1,0,0], [0,1,0,0,1,1,0,0,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,0,0,0,0,0,0,1,1,0,0,0,0]] -
输出:
6
解题思路:
BFS遍历 + 面积计数:
- 初始化访问标记数组,遍历每个网格点;
- 若当前点是陆地且未访问,启动BFS并统计该岛屿的面积;
- BFS逻辑:
- 起始坐标入队、标记已访问,初始化面积计数器
count = 1; - 遍历四个方向,每找到一个未访问的陆地,计数器+1并入队标记;
- 起始坐标入队、标记已访问,初始化面积计数器
- 维护全局最大面积
ret,遍历结束后返回。
完整代码:
cpp
class Solution {
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n;
bool vis[51][51] = {false}; // 访问标记
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
m = grid.size(), n = grid[0].size();
int ret = 0;
// 遍历所有网格点
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
{
if(grid[i][j] == 1 && !vis[i][j])
{
// 更新最大面积
ret = max(ret, bfs(grid, i, j));
}
}
return ret;
}
int bfs(vector<vector<int>>& grid, int i, int j)
{
queue<pair<int, int>> q;
q.push({i, j});
vis[i][j] = true;
int count = 1; // 初始面积为1(当前点)
while(!q.empty())
{
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 && y >= 0 && x < grid.size() && y < grid[0].size() && grid[x][y] == 1 && !vis[x][y])
{
vis[x][y] = true;
q.push({x, y});
count++; // 面积+1
}
}
}
return count;
}
};
复杂度分析:
- 时间复杂度:O(mn)O(mn)O(mn),每个网格点仅遍历一次。
- 空间复杂度:O(mn)O(mn)O(mn),访问标记数组 + 队列(最坏情况存储最大岛屿的所有陆地)。
四、被围绕的区域
题目描述:
给定一个由 'X' 和 'O' 组成的二维网格,将所有被 'X' 围绕的 'O' 替换为 'X'(边界上的 'O' 及其连通的 'O' 不会被围绕,需保留)。
示例:
-
输入:
[["X","X","X","X"], ["X","O","O","X"], ["X","X","O","X"], ["X","O","X","X"]] -
输出:
[["X","X","X","X"], ["X","X","X","X"], ["X","X","X","X"], ["X","O","X","X"]]
解题思路:
反向思维 + BFS标记:
- 核心逻辑:仅保留边界连通的'O',其余'O'替换为'X';
- 遍历网格的所有边界坐标:
- 若边界坐标是
'O',启动BFS,将其及连通的'O'标记为临时字符(如'.');
- 若边界坐标是
- 遍历整个网格:
- 将未标记的
'O'替换为'X'; - 将标记的
'.'还原为'O'。
- 将未标记的
完整代码:
cpp
class Solution {
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n;
public:
void solve(vector<vector<char>>& board) {
m = board.size(), n = board[0].size();
// 遍历边界:左右列
for(int i = 0; i < m; i++)
{
if(board[i][0] == 'O') bfs(board, i, 0);
if(board[i][n - 1] == 'O') bfs(board, i, n - 1); // 修正原代码笔误:n-1而非m-1
}
// 遍历边界:上下行
for(int j = 0; j < n; j++)
{
if(board[0][j] == 'O') bfs(board, 0, j);
if(board[m - 1][j] == 'O') bfs(board, m - 1, j);
}
// 替换字符
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
{
if(board[i][j] == 'O') board[i][j] = 'X'; // 被围绕的O替换为X
if(board[i][j] == '.') board[i][j] = 'O'; // 边界连通的O还原
}
}
void bfs(vector<vector<char>>& board, int i, int j)
{
queue<pair<int, int>> q;
q.push({i, j});
board[i][j] = '.'; // 标记为临时字符
while(!q.empty())
{
auto [a, b] = q.front();
q.pop();
// 遍历四个方向
for(int i = 0; i < 4; i++)
{
int x = a + dx[i], y = b + dy[i];
// 边界检查 + 未标记的O
if(x >= 0 && y >= 0 && x < board.size() && y < board[0].size() && board[x][y] == 'O')
{
board[x][y] = '.';
q.push({x, y});
}
}
}
}
};
注:原代码中存在笔误
board[i][n - 1] == 'O') bfs(board, i, m - 1),已修正为n - 1,否则会导致列坐标越界。
复杂度分析:
- 时间复杂度:O(mn)O(mn)O(mn),每个网格点仅遍历两次(边界BFS + 全局替换)。
- 空间复杂度:O(mn)O(mn)O(mn),队列最坏情况存储所有边界连通的'O'。
二维网格BFS通用框架总结
所有网格类BFS问题均可复用以下核心框架:
cpp
// 1. 定义方向向量(上下左右)
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
// 2. BFS核心函数
void bfs(网格类型& grid, 起始坐标i, 起始坐标j) {
queue<pair<int, int>> q;
q.push({i, j});
标记起始点(访问/临时字符/颜色);
while(!q.empty()) {
auto [a, b] = q.front();
q.pop();
// 遍历四个方向
for(int k = 0; k < 4; k++) {
int x = a + dx[k], y = b + dy[k];
// 3. 边界检查 + 有效节点判断(未访问/目标颜色/陆地等)
if(x >= 0 && y >= 0 && x < 网格行数 && y < 网格列数 && 节点有效条件) {
标记节点;
q.push({x, y});
}
}
}
}