LeetCode刷题专题:FloodFill泛滥填充算法剖析
题目1:图像渲染(LeetCode 733)
1. 题目描述

提示:
m == image.lengthn == image[i].length1 <= m, n <= 500 <= image[i][j], color < 2160 <= sr < m0 <= sc < n
2. 核心算法思路
本题是经典Flood Fill洪水填充算法,二选一即可实现:深度优先搜索 DFS(递归写法,本题代码采用);广度优先搜索 BFS(队列迭代写法)
通用逻辑:遍历当前像素上下左右4个方向所有相连、且原始像素值相同的点,批量修改颜色,遍历完所有连通区域即完成渲染。
递归DFS函数设计
1) 函数参数
a. 原始二维图像矩阵:待修改的图片数组
b. 当前遍历坐标 (i,j):正在处理的像素位置
c. 目标修改颜色 newColor:最终要替换成的像素值
额外全局记录:原始初始像素颜色 prev(防止修改后颜色错乱,重复递归)
2) 递归函数体执行流程
1. 先把当前位置像素,直接改成目标颜色(前置判断保证:所有进入递归的坐标,一定是需要修改的合法点)
2. 遍历当前点上下左右4个方向的相邻坐标
3. 合法性判断:坐标不越界(在矩阵行数、列数范围内);像素原始颜色 = 初始点颜色
4. 满足条件 → 递归深入处理该相邻像素
class Solution
{
// 上下左右 4个方向偏移量,x行偏移,y列偏移
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n; // 矩阵行数、列数
int prev; // 记录起点原始像素颜色,避免递归死循环
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color)
{
// 特殊情况:起点颜色本来就是目标颜色,无需修改,直接返回原图
if(image[sr][sc] == color) return image;
m = image.size(); // 获取矩阵总行数
n = image[0].size(); // 获取矩阵总列数
prev = image[sr][sc]; // 保存起点原本的像素值
dfs(image, sr, sc, color); // 递归深度优先遍历渲染
return image;
}
// DFS递归核心函数
void dfs(vector<vector<int>>& image, int i, int j, int color)
{
image[i][j] = color; // 第一步:直接修改当前像素颜色
// 遍历4个方向
for(int k = 0; k < 4; k++)
{
// 计算相邻点新坐标
int x = i + dx[k];
int y = j + dy[k];
// 坐标合法 + 像素是原始颜色 → 递归处理
if(x >= 0 && x < m && y >= 0 && y < n && image[x][y] == prev)
{
dfs(image, x, y, color);
}
}
}
};
3. 核心知识点&易错重点总结
1) 四连通规则
本题只认上下左右4个方向,斜对角像素不属于连通区域,这是和八连通填充的核心区别。
2) 为什么要提前保存原始颜色prev?
如果不保存原始颜色,直接修改当前像素后,后续递归判断image[x][y] == 原颜色会失效,同时会出现无限递归死循环。
3) 递归终止隐含逻辑
坐标超出矩阵边界,自动停止递归;像素颜色不等于原始颜色,不进入递归;天然实现DFS遍历结束,无需额外写return终止语句。
4) 复杂度分析
时间复杂度:O(m×n),最坏情况整张图片所有像素都需要遍历修改
空间复杂度:O(m×n),最坏递归深度等于整张图片像素数量
5) 同类拓展
Flood Fill算法广泛用于:图像抠图、区域染色、迷宫寻路、岛屿数量统计、连通块计数题目
题目2:岛屿数量(LeetCode 200)
1. 题目描述

提示:
m == grid.lengthn == grid[i].length1 <= m, n <= 300grid[i][j]的值为'0'或'1'
2. 核心算法思路
本题是Flood Fill洪水填充算法的经典应用,和「733.图像渲染」是同一系列的连通块问题。
通用逻辑
遍历整个矩阵,每次遇到一块未访问的陆地:
1) 说明找到一个新岛屿,将岛屿数量 ret +1
2) 用DFS/BFS将这块岛屿的所有连通陆地标记为「已访问」(或直接修改为水)
3) 遍历完成后,ret 就是岛屿总数
DFS 算法流程
1) 初始化岛屿数量 ret = 0
2) 双重循环遍历二维网格的每个格子:
若当前格子是未访问的陆地(grid[i][j] == '1' 且未被访问):
ret++(岛屿数量+1)
调用DFS,将该岛屿的所有连通陆地标记为已访问
3) 遍历完成后返回 ret
递归DFS函数设计
核心步骤:标记当前格子为已访问;向上下左右4个方向递归寻找相邻陆地,满足以下条件才进入递归:坐标不越界,格子未被访问,格子值为 '1'(陆地)
class Solution
{
vector<vector<bool>> vis; // 标记已访问的格子
int m, n; // 矩阵行数、列数
public:
int numIslands(vector<vector<char>>& grid)
{
m = grid.size();
if (m == 0) return 0;
n = grid[0].size();
vis = vector<vector<bool>>(m, vector<bool>(n));
int ret = 0; // 岛屿数量
for (int i = 0; i < m; i++)
{
for (int j = 0; j < n; j++)
{
// 遇到未访问的陆地,开始填充
if (!vis[i][j] && grid[i][j] == '1')
{
ret++;
dfs(grid, i, j);
}
}
}
return ret;
}
// 上下左右四个方向的偏移量
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
void dfs(vector<vector<char>>& grid, int i, int j)
{
vis[i][j] = true; // 标记为已访问
for (int k = 0; k < 4; k++)
{
// 计算相邻格子的坐标
int x = i + dx[k];
int y = j + dy[k];
// 坐标合法、未访问、是陆地 → 递归填充
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] == '1')
{
dfs(grid, x, y);
}
}
}
};
3. 关键知识点&易错点
1) 核心思想:标记访问避免重复统计
每次遇到未访问的陆地,就把整个连通块标记为已访问,这样后续遍历就不会重复统计同一个岛屿。
写法1:用额外的 vis 数组标记(代码中使用的方式,不修改原数据)
写法2:直接把访问过的陆地改为 '0'(节省空间,修改原数据)
2) 四连通规则
岛屿的连通性只看上下左右四个方向,斜对角不算连通,和「图像渲染」的连通规则一致。
3) 复杂度分析
时间复杂度:O(m×n),每个格子最多被访问一次
空间复杂度:O(m×n),最坏情况(全是陆地)递归深度为矩阵大小
4) 与「图像渲染」的关系
本题的DFS逻辑和「733.图像渲染」几乎完全一致:图像渲染:把所有连通的同色格子改成目标色;岛屿数量:把所有连通的陆地格子标记为已访问,统计连通块数量;本质都是Flood Fill 洪水填充算法的应用。
4. 拓展:BFS解法思路(队列实现)
如果担心DFS递归深度过大导致栈溢出,可以用BFS队列实现:
1) 遇到陆地时,将坐标加入队列
2) 不断取出队首元素,标记为已访问,并将其四个方向的合法陆地加入队列
3) 队列为空时,当前岛屿的所有陆地已被标记完成
题目3:岛屿的最大面积(LeetCode 695)
1. 题目描述

2. 核心算法思路
本题依然是 Flood Fill 洪水填充算法 的经典应用,和「200.岛屿数量」是同一系列的连通块问题。
通用逻辑
1) 遍历整个矩阵,每次遇到一块未访问的陆地(grid[i][j] == 1)
2) 用 DFS/BFS 计算该岛屿的面积(连通块内所有 1 的数量)
3) 维护一个全局变量,记录所有岛屿面积中的最大值
4) 遍历完成后,返回最大值(没有岛屿则为 0)
主函数流程
1) 初始化最大面积 ret = 0
2) 双重循环遍历矩阵每个格子:若当前格子是未访问的陆地,调用 DFS 计算该岛屿的面积;将计算出的面积与 ret 比较,更新 ret 为较大值
3) 遍历完成后返回 ret
DFS递归函数流程
1) 标记当前格子为已访问
2) 初始化面积计数 count += 1(当前格子面积为1)
3) 向上下左右四个方向递归:若相邻格子合法、未访问且为陆地,则递归处理,并累加面积
4) 递归结束后,count 即为当前岛屿的总面积
class Solution
{
bool vis[51][51]; // 标记已访问的格子
int m, n; // 矩阵行数、列数
int dx[4] = {0, 0, 1, -1}; // 上下左右四个方向偏移量
int dy[4] = {1, -1, 0, 0};
int count; // 记录当前岛屿的面积
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 (!vis[i][j] && grid[i][j] == 1)
{
count = 0; // 重置当前岛屿面积
dfs(grid, i, j);
ret = max(ret, count); // 更新最大面积
}
}
}
return ret;
}
void dfs(vector<vector<int>>& grid, int i, int j)
{
count++; // 当前格子面积+1
vis[i][j] = true; // 标记为已访问
// 遍历四个方向
for (int k = 0; k < 4; k++)
{
int x = i + dx[k];
int y = j + dy[k];
// 坐标合法、未访问、是陆地 → 递归处理
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && grid[x][y] == 1)
{
dfs(grid, x, y);
}
}
}
};
3. 关键知识点&易错点
1) 与「岛屿数量」的核心区别
岛屿数量:只需要统计连通块的个数
岛屿的最大面积:不仅要统计连通块,还要计算每个连通块的大小,并记录最大值
2) 两种标记访问的方式
方式1:额外开辟 vis 数组(代码中使用的方式,不修改原数据,更安全)
方式2:直接将访问过的 1 修改为 0(节省空间,但会修改原矩阵)
3) 复杂度分析
时间复杂度:O(m×n),每个格子最多被访问一次
空间复杂度:O(m×n),最坏情况(全是陆地)递归深度为矩阵大小
4) 易错点提醒
必须注意四连通规则:斜对角的 1 不算连通,示例1中答案不是11,就是因为斜对角的陆地不属于同一个岛屿
每次遇到新岛屿时,要重置面积计数变量 count,避免不同岛屿的面积互相干扰
题目4:被围绕的区域(LeetCode 130)
1. 题目描述

2. 核心思路:正难则反
直接找“被包围的区域”比较困难,我们反过来思考:
1) 标记安全区域:从矩阵的四条边界出发,用 DFS 遍历所有与边界相连的 'O',将它们临时标记为 '.'(表示这些 'O' 是安全的,不会被填充)。
2) 填充被包围区域:遍历整个矩阵:
遇到 'O':说明它没有被标记,是被包围的区域,直接改为 'X'。
遇到 '.':说明它是安全区域,恢复为 'O'。
| 细节 | 说明 |
| 越界检查 | DFS中必须先判断 x >= 0 && x < m && y >= 0 && y < n,否则会访问矩阵外的内存导致崩溃。 |
| 临时标记 | 用.标记安全的O,避免重复遍历;如果直接修改为X,会丢失边界O的信息。 |
| 边界遍历 | 四条边界都要遍历,不能遗漏任何一条,否则会漏掉部分安全O。 |
| 递归终止条件 | DFS遇到非O(X或.)时就停止,避免无限递归。 |
3) 完整 C++ 代码
class Solution
{
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
int m, n;
public:
void solve(vector<vector<char>>& board)
{
m = board.size(), n = board[0].size();
// 1. 把边界的 O 相连的联通块,全部修改成 .
for(int j = 0; j < n; j++)
{
if(board[0][j] == 'O') dfs(board, 0, j);
if(board[m - 1][j] == 'O') dfs(board, m - 1, j);
}
for(int i = 0; i < m; i++)
{
if(board[i][0] == 'O') dfs(board, i, 0);
if(board[i][n - 1] == 'O') dfs(board, i, n - 1);
}
// 2. 还原
for(int i = 0; i < m; i++)
{
for(int j = 0; j < n; j++)
{
if(board[i][j] == '.') board[i][j] = 'O';
else if(board[i][j] == 'O') board[i][j] = 'X';
}
}
}
void dfs(vector<vector<char>>& board, int i, int j)
{
board[i][j] = '.';
for(int k = 0; k < 4; k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'O')
{
dfs(board, x, y);
}
}
}
};
题目5:太平洋大西洋水流问题(LeetCode 417)
1. 题目描述

2. 核心思路:反向 DFS
直接判断“能否流向两个海洋”会重复遍历大量路径,效率低下。我们反向思考:
1) 反向标记可达太平洋的单元格:从太平洋沿岸(上、左边界)出发,反向 DFS(允许从低到高或等高流动),标记所有能反向流到太平洋的单元格。
2) 反向标记可达大西洋的单元格:从大西洋沿岸(下、右边界)出发,反向 DFS,标记所有能反向流到大西洋的单元格。
3) 求交集:同时被两个标记矩阵标记的单元格,就是既能流向太平洋、又能流向大西洋的结果。
class Solution
{
int m, n;
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
public:
vector<vector<int>> pacificAtlantic(vector<vector<int>>& h)
{
m = h.size(), n = h[0].size();
vector<vector<bool>> pac(m, vector<bool>(n));
vector<vector<bool>> atl(m, vector<bool>(n));
// 1. 先处理 pac 洋
for(int j = 0; j < n; j++) dfs(h, 0, j, pac);
for(int i = 0; i < m; i++) dfs(h, i, 0, pac);
// 2. 处理 atl 洋
for(int i = 0; i < m; i++) dfs(h, i, n - 1, atl);
for(int j = 0; j < n; j++) dfs(h, m - 1, j, atl);
vector<vector<int>> ret;
for(int i = 0; i < m; i++)
for(int j = 0; j < n; j++)
if(pac[i][j] && atl[i][j])
ret.push_back({i, j});
return ret;
}
void dfs(vector<vector<int>>& h, int i, int j, vector<vector<bool>>& vis)
{
vis[i][j] = true;
for(int k = 0; k < 4; k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && h[x][y] >= h[i][j])
dfs(h, x, y, vis);
}
}
};
3. 核心知识点总结
1) 正难则反思想
两道题都用了反向遍历的技巧:
被围绕的区域:不直接找被包围的 'O',而是先标记安全的 'O'。
水流问题:不直接找能流向海洋的单元格,而是从海洋反向找能到达的单元格。
这种方法避免了重复遍历和复杂的路径判断,大幅提升了效率。
2) DFS 通用模板
两道题的 DFS 逻辑高度相似,可总结为以下模板:
// 四个方向偏移量
int dx[4] = {1, -1, 0, 0};
int dy[4] = {0, 0, 1, -1};
void dfs(矩阵/数组, 当前位置, 标记矩阵/临时状态) {
// 1. 边界/终止条件判断
if (越界 || 已访问 || 不满足条件) return;
// 2. 标记当前状态(已访问/临时修改)
标记当前位置为已访问/安全;
// 3. 递归遍历四个方向
for (int k = 0; k < 4; k++) {
int x = i + dx[k];
int y = j + dy[k];
dfs(矩阵/数组, x, y, 标记矩阵/临时状态);
}
}
3) 矩阵遍历的边界处理
遍历边界时,注意区分四条边:上边界(i=0)、下边界(i=m-1)、左边界(j=0)、右边界(j=n-1)。
反向 DFS 时,要注意流动条件的反转(如水流问题中,从“从高到低”变为“从低到高/等高”)。
题目6:扫雷游戏(LeetCode 529)
1. 题目描述

提示:
m == board.lengthn == board[i].length1 <= m, n <= 50board[i][j]为'M'、'E'、'B'或数字'1'到'8'中的一个click.length == 20 <= clickr < m0 <= clickc < nboard[clickr][clickc]为'M'或'E'
2. 解法
算法思路:模拟类型的DFS题目。首先要搞懂题目要求(游戏规则),从题目所给的点击位置开始,根据游戏规则,来一次DFS即可。
1)先搞懂题目要我们做什么
我们要模拟扫雷游戏里“点击一下棋盘”后的变化。棋盘上有几种格子:
M:没挖开的地雷
E:没挖开的空白格子
B:已经挖开、周围没有地雷的空白格子
数字1-8:已经挖开、周围有对应数量地雷的格子
X:被挖开的地雷(游戏结束)
我们的任务,就是根据点击的位置,按照游戏规则更新整个棋盘的状态。
2)分情况处理点击的位置
情况1:直接点到了地雷(M)
这是最简单的情况:游戏直接结束。
只需要把这个格子改成X,然后直接返回更新后的棋盘就行,其他格子都不用动。
情况2:点到了空白格子(E)
这是最复杂的情况,我们需要分两步处理:
第一步:数清楚这个格子周围有多少个地雷
我们要检查这个格子的8个相邻方向(上下左右+4个对角线),数出里面有多少个M。
这一步是判断接下来要做什么的关键。
第二步:根据地雷数量,执行不同的规则
1. 如果周围有地雷(数量 ≥ 1)
这个格子不能继续扩展,只能显示地雷的数量。
我们把这个格子改成对应的数字字符(比如周围有2个雷,就改成'2'),然后就不用再处理其他格子了。
2. 如果周围没有地雷(数量 = 0)
这个格子要被标记为B,并且触发“连锁展开”效果:所有和它连通的、没挖开的空白格子(E)都要被自动挖开。
怎么做连锁展开?我们就对这个格子的8个相邻方向,逐个检查:
如果相邻的格子是E,就对它重复上面的“数雷-判断-处理”流程(也就是递归处理)。
如果相邻的格子已经被挖开(B、数字)或者是地雷(M),就跳过,不用管它。
3)整个流程的核心逻辑
你可以把整个过程理解成:
1. 先判断点击的是不是地雷,是就直接结束。
2. 如果是空白格子,先数它周围的地雷数量。
3. 有雷就改成数字,结束;没雷就改成B,然后把周围所有能连通的空白格子都按同样的规则处理一遍。
4. 一直重复,直到所有能挖开的格子都被处理完,返回最终的棋盘状态。
C++ 算法代码
class Solution
{
int dx[8] = {0, 0, 1, -1, 1, 1, -1, -1};
int dy[8] = {1, -1, 0, 0, 1, -1, 1, -1};
int m, n;
public:
vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click)
{
m = board.size(), n = board[0].size();
int x = click[0], y = click[1];
if (board[x][y] == 'M') // 直接点到地雷
{
board[x][y] = 'X';
return board;
}
dfs(board, x, y);
return board;
}
void dfs(vector<vector<char>>& board, int i, int j)
{
// 统计一下周围的地雷个数
int count = 0;
for(int k = 0; k < 8; k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'M')
{
count++;
}
}
if(count) // 周围有地雷
{
board[i][j] = count + '0';
return;
}
else // 周围没有地雷
{
board[i][j] = 'B';
for(int k = 0; k < 8; k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'E')
{
dfs(board, x, y);
}
}
}
}
};
题目7:衣橱整理(LeetCode LCR 130)/ 机器人的运动范围
1. 题目描述

地上有一个 m 行 n 列的方格,从坐标 [0,0] 到坐标 [m-1,n-1]。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于 k 的格子。
例如:当 k 为18时,机器人能够进入方格 [35,37],因为 3+5+3+7=18。但它不能进入方格 [35,38],因为 3+5+3+8=19。请问该机器人能够到达多少个格子?
示例1
输入:m = 2, n = 3, k = 1 输出:3
示例2
输入:m = 3, n = 1, k = 0 输出:1
提示
• 1 <= n, m <= 100
• 0 <= k <= 20
2. 算法思路
这是一道非常典型的「搜索」类问题。我们可以通过「深搜(DFS)」或者「宽搜(BFS)」,从 [0, 0] 点出发,按照题目的「规则」一直往 [m - 1, n - 1] 位置走。
同时,设置一个全局变量,每次走到一个合法位置,就将全局变量加一。当我们把所有能走到的路都走完之后,全局变量里面存的就是最终答案。
3. 解法(DFS)
递归函数设计
a. 参数:当前所在的位置 [i, j],行走的边界 [m, n],坐标数位之和的边界 k
b. 递归出口:
i. [i, j] 的坐标不合法(超出能走的范围)
ii. [i, j] 位置已经走过了(需要创建一个全局变量 bool st[101][101],标记当前位置是否走过)
iii. [i, j] 坐标的数位之和大于 k
上述情况的任何一种都是递归出口
c. 函数体内部:
i. 如果这个坐标是合法的,就将全局变量 ret++
ii. 标记 [i, j] 位置已经遍历过
iii. 向 [i, j] 位置的上下左右四个方向继续搜索
辅助函数
a. 检测坐标 [i, j] 是否合法
b. 计算出 i, j 的数位之和,然后与 k 作比较
主函数
a. 调用递归函数,从 [0, 0] 点出发
辅助的全局变量
a. 二维数组 bool st[101][101]:标记 [i, j] 位置是否已经遍历过
b. 变量 ret:记录一共到达多少个合法的位置
c. 上下左右的四个坐标变换
这道题是 LCR 130. 衣橱整理,本质就是「机器人的运动范围」,只是换了个“整理衣橱”的场景:衣橱被划分为 m 行 n 列的格子;整理师从 (0,0) 出发,只能向右/向下移动;不能移动到衣柜外,也不能整理坐标数位和大于 cnt 的格子;求能整理的格子总数
解题思路:和之前的机器人运动范围思路完全一致,用DFS/BFS搜索即可
核心逻辑
1. 数位和计算:先写一个辅助函数,计算一个数字的各位数字之和(比如 35 → 3+5=8)
2. DFS 搜索:
从 (0,0) 出发,每次尝试向右/向下走一步
每走到一个格子,先判断是否合法(没越界、没走过、数位和≤cnt)
合法就计数+1,标记为已访问,再递归搜索下一个格子
3. 边界剪枝:越界、已访问、数位和超标的格子直接跳过,不继续递归
class Solution {
public:
// 记录是否访问过
bool vis[101][101] = {false};
int m, n, cnt;
int res = 0;
// 方向:向右、向下
int dx[2] = {1, 0};
int dy[2] = {0, 1};
// 计算数字的数位和
int digitSum(int x) {
int sum = 0;
while(x) {
sum += x % 10;
x /= 10;
}
return sum;
}
void dfs(int i, int j) {
// 越界判断
if(i < 0 || i >= m || j < 0 || j >= n) return;
// 已访问过
if(vis[i][j]) return;
// 数位和超过限制
if(digitSum(i) + digitSum(j) > cnt) return;
// 标记已访问,计数+1
vis[i][j] = true;
res++;
// 向右、向下递归
for(int k = 0; k < 2; k++) {
int x = i + dx[k];
int y = j + dy[k];
dfs(x, y);
}
}
int wardrobeFinishing(int m, int n, int cnt) {
this->m = m;
this->n = n;
this->cnt = cnt;
dfs(0, 0);
return res;
}
};
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)