递归专题5 - FloodFill算法专题
递归专题5 - FloodFill算法专题
本文是递归算法系列的第5篇,完整体系包括:
- 递归基础与思维方法
- 二叉树DFS专题
- 回溯算法十大类型
- 网格DFS与回溯
- FloodFill算法专题 (本文)

📋 目录
- 前言
- 一、FloodFill的本质
- 1.1 什么是FloodFill
- 1.2 FloodFill vs 网格DFS
- 二、FloodFill的通用模板
- 2.1 基础模板
- 2.2 模板要点
- 三、FloodFill的六种应用
- 应用1: 图像渲染 (LeetCode 733)
- 应用2: 岛屿数量 (LeetCode 200)
- 应用3: 岛屿最大面积 (LeetCode 695)
- 应用4: 被围绕的区域 (LeetCode 130)
- 应用5: 太平洋大西洋水流 (LeetCode 417)
- 应用6: 扫雷游戏 (LeetCode 529)
- 四、FloodFill总结
- 4.1 FloodFill vs 网格DFS
- 4.2 FloodFill的六种应用
- 4.3 常见错误总结
- 五、总结
前言
FloodFill是图像处理中的"油漆桶"工具,也是网格DFS的一个特殊应用。做岛屿问题的时候,一开始把它当成普通的网格DFS来做,结果发现有些地方不太一样。最关键的区别是:FloodFill不需要回溯,不需要恢复现场。
做了六道FloodFill题后,总结出了它的核心特点和六种常见应用。这篇文章记录这些经验,以及踩过的坑。
一、FloodFill的本质
1.1 什么是FloodFill
FloodFill就是从一个起点出发,把所有连通的相同区域填充成新颜色。
想象一下画图软件的油漆桶:
点击一个格子 -> 这个格子和相邻的相同颜色格子都被填充
1.2 FloodFill vs 网格DFS
| 特性 | 网格DFS (单词搜索) | FloodFill |
|---|---|---|
| 目标 | 找路径 | 填充区域 |
| 需要回溯 | ✅ 需要 | ❌ 不需要 |
| vis数组 | 需要恢复 | 不需要恢复 |
| 修改原数组 | 一般不改 | 直接修改 |
核心区别:
网格DFS:需要尝试多条路径 -> 要回溯 -> 要恢复vis
vis[i][j] = true;
dfs(...);
vis[i][j] = false; // 恢复
FloodFill:只走一次,填充后不回头 -> 不需要回溯
image[i][j] = newColor; // 修改后不恢复
dfs(...);
// 不需要恢复
为什么不需要回溯?
因为FloodFill的目的是"填充",而不是"搜索路径"。填充后的格子不需要再访问,所以不需要恢复。
二、FloodFill的通用模板
2.1 基础模板
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};void dfs(grid, int i, int j, int oldValue, int newValue) {// 出口1:越界if (i < 0 || i >= m || j < 0 || j >= n) return;// 出口2:不是目标颜色 (已填充或其他颜色)if (grid[i][j] != oldValue) return;// 填充当前格子grid[i][j] = newValue;// 四个方向递归for (int k = 0; k < 4; k++) {dfs(grid, i + dx[k], j + dy[k], oldValue, newValue);}// 不需要恢复现场!
}
2.2 模板要点
1. 直接修改原数组
grid[i][j] = newValue;
// 修改本身就是"标记已访问"
2. 不需要vis数组
为什么?因为修改颜色 = 标记已访问
// 传统网格DFS (需要vis)
if (vis[i][j]) return;
vis[i][j] = true;// FloodFill (不需要vis)
if (grid[i][j] != oldValue) return; // 修改过的自然不满足
grid[i][j] = newValue;
3. 不需要恢复现场
// 网格DFS (需要恢复)
vis[i][j] = true;
dfs(...);
vis[i][j] = false; // 恢复// FloodFill (不恢复)
grid[i][j] = newValue;
dfs(...);
// 不恢复
三、FloodFill的六种应用
做了六道题后,总结出FloodFill的六种常见应用:
| 题目 | 难度 | 类型 | 核心技巧 |
|---|---|---|---|
| LeetCode 733 | Easy | 填充一个区域 | 基础模板 |
| LeetCode 200 | Medium | 计数区域数量 | 全局遍历+计数 |
| LeetCode 695 | Medium | 求最大区域面积 | void+全局变量 |
| LeetCode 130 | Medium | 反向标记 | 从边界出发 |
| LeetCode 417 | Medium | 双边界+交集 | 两个vis数组 |
| LeetCode 529 | Medium | 8方向+条件递归 | 统计周围状态 |
应用1:图像渲染 (LeetCode 733)
**问题:**从起点出发,把所有连通的相同颜色区域填充成新颜色
代码:
class Solution {
public:int m, n;int oldColor;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();oldColor = image[sr][sc];dfs(image, sr, sc, color);return image;}void dfs(vector<vector<int>>& image, int i, int j, int color) {// 出口1:越界if (i < 0 || i >= m || j < 0 || j >= n) return;// 出口2:颜色不是oldColorif (image[i][j] != oldColor) return;// 填充image[i][j] = color;// 四个方向dfs(image, i-1, j, color);dfs(image, i+1, j, color);dfs(image, i, j-1, color);dfs(image, i, j+1, color);}
};
关键点:
- 特判
oldColor == color
如果初始色就是目标色,会导致无限递归:
oldColor = 2, newColor = 2dfs(i, j):image[i][j] = 2if (image[i][j] != oldColor) return // 2 != 2? false继续递归 -> 无限循环
所以主函数要先判断:
if (image[sr][sc] == color) return image;
- 判断条件是
!= oldColor而不是== color
错误写法:
if (image[i][j] == color) return; // 只判断是否已填充
问题:遇到其他颜色时,虽然不会修改,但会继续递归,浪费时间。
正确写法:
if (image[i][j] != oldColor) return; // 判断是否是要填充的颜色
这样遇到其他颜色(包括已填充的)都会直接返回。
应用2:岛屿数量 (LeetCode 200)
**问题:**计算网格中有多少个岛屿 (1的连通区域)
思路:
岛屿数量 = FloodFill的次数遍历所有格子:遇到未访问的1:岛屿计数+1用FloodFill"沉没"整个岛屿
代码:
class Solution {
public:bool vis[301][301];int m, n;int numIslands(vector<vector<char>>& grid) {m = grid.size();n = grid[0].size();memset(vis, false, sizeof(vis));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]) {dfs(grid, i, j);ret++; // 发现一个新岛屿}}}return ret;}void dfs(vector<vector<char>>& grid, int i, int j) {if (i < 0 || i >= m || j < 0 || j >= n) return;if (vis[i][j] || grid[i][j] == '0') return;vis[i][j] = true; // 标记为已访问dfs(grid, i-1, j);dfs(grid, i+1, j);dfs(grid, i, j-1);dfs(grid, i, j+1);}
};
常见错误:
- 字符 vs 整数比较错误
// 错误!
if (grid[i][j] == 0) return; // grid是vector<vector<char>>// 正确
if (grid[i][j] == '0') return; // 用字符'0'
为什么?
grid[i][j] = '0' -> ASCII码48
grid[i][j] = 0 -> 整数048 != 0,所以判断永远不成立
- 忘记花括号导致逻辑错误
// 错误
if (grid[i][j] == '1' && !vis[i][j])dfs(grid, i, j); // 在if内ret++; // 不在if内!每次循环都执行// 正确
if (grid[i][j] == '1' && !vis[i][j]) {dfs(grid, i, j);ret++; // 都在if内
}
应用3:岛屿最大面积 (LeetCode 695)
**问题:**返回最大的岛屿面积
思路:
遍历所有岛屿,求最大面积类似Day28的黄金矿工:
- 用void返回值
- 用全局变量tmp累加当前岛屿面积
- 用全局变量ret记录最大面积
代码:
class Solution {
public:int ret = 0; // 最大面积int tmp = 0; // 当前岛屿面积int m, n;bool vis[51][51];int maxAreaOfIsland(vector<vector<int>>& grid) {m = grid.size();n = grid[0].size();memset(vis, false, sizeof(vis));for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1 && !vis[i][j]) {dfs(grid, i, j);ret = max(ret, tmp);tmp = 0; // 重置tmp}}}return ret;}void dfs(vector<vector<int>>& grid, int i, int j) {if (i < 0 || i >= m || j < 0 || j >= n) return;if (vis[i][j] || grid[i][j] == 0) return;tmp++; // 累加面积vis[i][j] = true;dfs(grid, i-1, j);dfs(grid, i+1, j);dfs(grid, i, j-1);dfs(grid, i, j+1);}
};
关键点:
- 每次进入新岛屿前重置
tmp = 0 - 每个格子都累加
tmp++ - DFS完一个岛屿后更新
ret = max(ret, tmp)
这和Day28的黄金矿工是同一个模式:void + 全局变量。
应用4:被围绕的区域 (LeetCode 130)
**问题:**把所有"被X包围的O"改成X
**难点:**怎么判断一个O是否"被围绕"?
反向思维:
正向:判断"被围绕" -> 判断"无法到达边界" -> 复杂反向:找"不被围绕" -> 从边界出发DFS -> 简单
算法:
1. 从四条边界出发,标记所有"不被围绕的O" (用'#'临时标记)
2. 遍历整个board:- O -> X (被围绕)- # -> O (不被围绕,恢复)
代码:
class Solution {
public:int m, n;void solve(vector<vector<char>>& board) {m = board.size();n = board[0].size();// Step 1:从边界出发标记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);}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);}// Step 2:统一处理for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (board[i][j] == 'O') board[i][j] = 'X';else if (board[i][j] == '#') board[i][j] = 'O';}}}void dfs(vector<vector<char>>& board, int i, int j) {if (i < 0 || i >= m || j < 0 || j >= n) return;if (board[i][j] != 'O') return;board[i][j] = '#'; // 临时标记dfs(board, i-1, j);dfs(board, i+1, j);dfs(board, i, j-1);dfs(board, i, j+1);}
};
关键点:
- 只遍历边界,不遍历所有格子
这是和前面题目的重要区别:
// 岛屿数量:遍历所有格子
for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {dfs(i, j);}
}// 被围绕区域:只遍历边界
for (int i = 0; i < m; i++) {dfs(i, 0); // 左边界dfs(i, n-1); // 右边界
}
for (int j = 0; j < n; j++) {dfs(0, j); // 上边界dfs(m-1, j); // 下边界
}
- 用临时标记’#’
为什么不直接改成X?因为需要区分:
- 原本的X
- 被围绕的O (要改成X)
- 不被围绕的O (要保留)
所以先用’#'标记"不被围绕的",最后统一处理。
应用5:太平洋大西洋水流 (LeetCode 417)
**问题:**找出能同时流向太平洋和大西洋的格子
**思路:**反向思维的进阶应用
正向:从每个格子判断能否到达两个大洋 -> 复杂反向:
1. 从太平洋边界出发,逆流而上,标记"能流向太平洋"的格子
2. 从大西洋边界出发,逆流而上,标记"能流向大西洋"的格子
3. 取交集
代码:
class Solution {
public:int m, n;int dx[4] = {-1, 1, 0, 0};int dy[4] = {0, 0, -1, 1};vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {m = heights.size();n = heights[0].size();// 两个vis数组vector<vector<bool>> pac(m, vector<bool>(n));vector<vector<bool>> atl(m, vector<bool>(n));// 从太平洋边界出发for (int i = 0; i < m; i++) dfs(heights, i, 0, pac);for (int j = 0; j < n; j++) dfs(heights, 0, j, pac);// 从大西洋边界出发for (int i = 0; i < m; i++) dfs(heights, i, n-1, atl);for (int j = 0; j < n; j++) dfs(heights, 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) {if (i < 0 || i >= m || j < 0 || j >= n) return;if (vis[i][j]) return;vis[i][j] = true;// 用dx/dy数组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 && h[x][y] >= h[i][j]) {dfs(h, x, y, vis);}}}
};
关键点:
- 两个vis数组
vector<vector<bool>> pac(m, vector<bool>(n)); // 太平洋
vector<vector<bool>> atl(m, vector<bool>(n)); // 大西洋
- 反向流动条件
// 正常流动:h[当前] >= h[下一个]
// 反向流动:h[下一个] >= h[当前]
if (h[x][y] >= h[i][j]) dfs(...);
- dx/dy数组的优势
传统写法需要4个if判断,容易出错:
// 传统写法:容易漏掉边界检查
if (h[i-1][j] >= h[i][j]) dfs(i-1, j); // i-1可能越界!
dx/dy写法统一处理:
// dx/dy写法:边界检查统一
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) { // 统一检查边界if (h[x][y] >= h[i][j]) dfs(...);}
}
应用6:扫雷游戏 (LeetCode 529)
**问题:**模拟扫雷游戏的点击操作
特殊点:
- 8方向搜索 (包含对角线)
- 条件递归 (只有周围没地雷才继续递归)
代码:
class Solution {
public:int m, n;int dx[8] = {0, 0, 1, -1, 1, 1, -1, -1}; // 8个方向int dy[8] = {1, -1, 0, 0, 1, -1, 1, -1};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];// 情况1:点到地雷if (board[x][y] == 'M') {board[x][y] = 'X';return board;}// 情况2:点到空格dfs(board, x, y);return board;}void dfs(vector<vector<char>>& board, int i, int j) {if (i < 0 || i >= m || j < 0 || j >= n) return;if (board[i][j] != 'E') return; // 不是未挖出的空格// 统计周围8个方向的地雷数int count = 0;for (int k = 0; k < 8; k++) {int x = i + dx[k];int y = j + dy[k];if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'M') {count++;}}// 根据count决定if (count > 0) {board[i][j] = count + '0'; // int转charreturn; // 有地雷,停止递归} else {board[i][j] = 'B'; // 没地雷,继续递归for (int k = 0; k < 8; k++) {int x = i + dx[k];int y = j + dy[k];dfs(board, x, y);}}}
};
关键点:
- 8方向搜索
// 4方向 (上下左右)
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};// 8方向 (上下左右 + 4个对角线)
int dx[8] = {0, 0, 1, -1, 1, 1, -1, -1};
int dy[8] = {1, -1, 0, 0, 1, -1, 1, -1};
- int转char
int count = 3;
board[i][j] = count + '0'; // 3 + 48 = 51 = '3'
- 条件递归
if (count > 0) {board[i][j] = count + '0';return; // 有地雷,停止
} else {board[i][j] = 'B';// 只有没地雷才继续递归for (8个方向) dfs(...);
}
这是FloodFill的特殊变体:不是无条件递归,而是根据周围状态决定是否递归。
四、FloodFill总结
4.1 FloodFill vs 网格DFS
| 特性 | 网格DFS | FloodFill |
|---|---|---|
| 目标 | 找路径/最优解 | 填充/标记区域 |
| 需要回溯 | ✅ | ❌ |
| vis恢复 | 需要 | 不需要 |
| 修改原数组 | 一般不改 | 直接修改 |
| 典型题目 | 单词搜索、黄金矿工 | 岛屿问题 |
核心理解:
- 网格DFS:需要尝试多条路径 → 要回溯
- FloodFill:只走一次,填充后不回头 → 不需要回溯
4.2 FloodFill的六种应用
| 应用 | 核心技巧 |
|---|---|
| 图像渲染 | 基础模板,注意特判 |
| 岛屿数量 | 全局遍历+计数 |
| 岛屿面积 | void+全局变量 |
| 被围绕区域 | 反向思维,从边界出发 |
| 太平洋大西洋 | 双边界,两个vis,取交集 |
| 扫雷游戏 | 8方向,条件递归 |
4.3 常见错误总结
- FloodFill误用回溯
// 错误:FloodFill不需要恢复
image[i][j] = color;
dfs(...);
image[i][j] = oldColor; // 不需要!
- 字符vs整数比较
// 错误
if (grid[i][j] == 0) // grid是char类型// 正确
if (grid[i][j] == '0') // 用字符'0'
- 忘记花括号
// 错误
if (...)dfs(...);ret++; // 不在if内// 正确
if (...) {dfs(...);ret++;
}
- 初始色==目标色时的无限递归
// 必须特判
if (image[sr][sc] == color) return image;
- 数组越界 (LeetCode 417)
// 错误:直接访问
if (h[i-1][j] >= h[i][j]) // i-1可能<0// 正确:先检查边界
int x = i - 1;
if (x >= 0 && x < m && h[x][j] >= h[i][j])
五、总结
FloodFill是网格DFS的简化版,核心特点是"不需要回溯":
为什么不需要回溯:
- 目的是填充,不是搜索路径
- 填充后不需要再访问
- 修改本身就是标记
六种应用:
- 基础填充 (图像渲染)
- 计数区域 (岛屿数量)
- 求最大面积 (void+全局变量)
- 反向标记 (被围绕区域)
- 双边界+交集 (太平洋大西洋)
- 8方向+条件递归 (扫雷游戏)
核心技巧:
- 直接修改原数组
- 不需要vis数组 (或不需要恢复)
- 反向思维 (从边界出发)
- dx/dy数组优化代码
- 4方向 vs 8方向
掌握这些模式和技巧,FloodFill的题基本就能快速解决。
系列文章
- 递归基础与思维方法
- 二叉树DFS专题
- 回溯算法十大类型
- 网格DFS与回溯
- FloodFill算法专题 (本文)
完
