文章目录
-
- 无需回头,只管蔓延
- [一、 前言:什么是 FloodFill?](#一、 前言:什么是 FloodFill?)
- [二、 图像渲染:FloodFill 的 Hello World](#二、 图像渲染:FloodFill 的 Hello World)
-
- [2.1 题目描述](#2.1 题目描述)
- [2.2 深度拆解:油漆桶工具的底层逻辑](#2.2 深度拆解:油漆桶工具的底层逻辑)
- [2.3 C++ 代码实战](#2.3 C++ 代码实战)
- [三、 岛屿数量:连通块的发现与摧毁](#三、 岛屿数量:连通块的发现与摧毁)
-
- [3.1 题目描述](#3.1 题目描述)
- [3.2 深度拆解:发现一个,消灭一个](#3.2 深度拆解:发现一个,消灭一个)
- [3.3 C++ 代码实战(使用 vis 数组标记版)](#3.3 C++ 代码实战(使用 vis 数组标记版))
- [四、 岛屿的最大面积:带着战利品归来](#四、 岛屿的最大面积:带着战利品归来)
-
- [4.1 题目描述](#4.1 题目描述)
- [4.2 深度拆解:全局变量的累加](#4.2 深度拆解:全局变量的累加)
- [4.3 C++ 代码实战](#4.3 C++ 代码实战)
- [五、 被围绕的区域:正难则反的智慧](#五、 被围绕的区域:正难则反的智慧)
-
- [5.1 题目描述](#5.1 题目描述)
- [5.2 深度拆解:边缘渗透法](#5.2 深度拆解:边缘渗透法)
- [5.3 C++ 代码实战](#5.3 C++ 代码实战)
- [六、 太平洋大西洋水流问题:逆流而上的多源搜索](#六、 太平洋大西洋水流问题:逆流而上的多源搜索)
-
- [6.1 题目描述](#6.1 题目描述)
- [6.2 深度拆解:逆向思维登峰造极](#6.2 深度拆解:逆向思维登峰造极)
- [6.3 C++ 代码实战](#6.3 C++ 代码实战)
- [七、 扫雷游戏:八方向的极限拉扯](#七、 扫雷游戏:八方向的极限拉扯)
-
- [7.1 题目描述](#7.1 题目描述)
- [7.2 深度拆解:规则驱动的 FloodFill](#7.2 深度拆解:规则驱动的 FloodFill)
- [7.3 C++ 代码实战](#7.3 C++ 代码实战)
- [八、 机器人的运动范围:带条件的 FloodFill](#八、 机器人的运动范围:带条件的 FloodFill)
-
- [8.1 题目描述](#8.1 题目描述)
- [8.2 深度拆解:连通性的陷阱](#8.2 深度拆解:连通性的陷阱)
- [8.3 C++ 代码实战](#8.3 C++ 代码实战)
- [九、 FloodFill 总结](#九、 FloodFill 总结)
无需回头,只管蔓延
一、 前言:什么是 FloodFill?
💬 开篇 :在前面的回溯算法中,我们反复强调了一个核心动作:恢复现场。因为我们要尝试所有的可能性(比如走迷宫找最短路、找所有解),走不通就退回来换条路。
🚀 核心破局 :但是,在二维网格中,有这样一类问题:我们只需要知道这片区域有多大 、有几个连通块 ,或者把相连的区域染色 。水流淌过的地方,自然就湿了,水不需要 退回去假装没流过!
这就是 FloodFill(洪水灌溉)算法。
💡 FloodFill 与回溯的本质区别:
- 标记即永恒 :一旦访问过(
vis[x][y] = true或修改了原数组),就再也不会去走第二遍,绝不恢复现场!- 时间复杂度极低 :因为每个格子最多只被访问一次,所以时间复杂度是稳稳的 O ( M × N ) O(M \times N) O(M×N)。
👍 点赞、收藏与分享:今天我们将用 7 道经典题,带你玩转 FloodFill 的各种变体(连通块、正难则反、多源搜索)。准备好了吗?
二、 图像渲染:FloodFill 的 Hello World
2.1 题目描述
题目链接 :733. 图像渲染
描述 :
给定一个二维整数数组
image表示图像的像素值,以及三个整数sr,sc和newColor。从像素
image[sr][sc]开始,把与它颜色相同且相连 (上下左右)的像素全部染成newColor。示例 :
输入:
image = [[1,1,1],[1,1,0],[1,0,1]], sr = 1, sc = 1, newColor = 2输出:
[[2,2,2],[2,2,0],[2,0,1]]
2.2 深度拆解:油漆桶工具的底层逻辑
我们在用画图软件的"油漆桶"时,点一下,周围颜色相同的区域全变色了。
这其实就是一个标准的 FloodFill 过程。
ASCII 状态蔓延图解:
bash
初始矩阵 (起点 1,1 值为 1):
1 1 1
1 1 0
1 0 1
第一步:染自己,向四周蔓延
1 2 1
2 2 0
1 0 1
最终态:所有相连的 1 都变成了 2
2 2 2
2 2 0
2 0 1
为什么不需要 vis 数组?
因为我们直接修改了原数组的值(image[x][y] = newColor)。在向外蔓延时,条件是 image[x][y] == prevColor。被染过色的格子已经不再是旧颜色,自然就不会被重复访问了。(注意特判:如果新颜色和旧颜色一样,会死循环,必须一开始就拦截!)
2.3 C++ 代码实战
cpp
class Solution {
private:
int dx[4] = {0, 0, 1, -1};
int dy[4] = {1, -1, 0, 0};
int m, n;
int prevColor;
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();
prevColor = image[sr][sc]; // 记录需要被替换的目标旧颜色
dfs(image, sr, sc, color);
return image;
}
void dfs(vector<vector<int>>& image, int i, int j, int color) {
// 1. 本层操作:染色(相当于标记已访问)
image[i][j] = color;
// 2. 向四个方向洪水灌溉
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] == prevColor) {
dfs(image, x, y, color);
// 注意:绝对没有恢复现场的代码!
}
}
}
};
三、 岛屿数量:连通块的发现与摧毁
3.1 题目描述
题目链接 :200. 岛屿数量
描述 :
给你一个由
'1'(陆地)和'0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,由相邻的陆地连接形成。
3.2 深度拆解:发现一个,消灭一个
如果我们在双重循环中遇到了一个 '1',说明我们发现了一个新岛屿,岛屿数量 +1。
但是,如果不做处理,下次循环到它旁边的 '1' 时,又会以为是新岛屿。
策略:沉岛战术
当我们发现一个 '1' 时,立刻启动 FloodFill 导弹,向四周蔓延,把相连的所有 '1' 全部炸成 '0'(或者用 vis 数组标记)。这样,这块岛屿就从地图上"消失"了,后续遍历绝不会重复计算。
3.3 C++ 代码实战(使用 vis 数组标记版)
cpp
class Solution {
private:
vector<vector<bool>> vis;
int m, n;
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size();
n = grid[0].size();
vis.assign(m, vector<bool>(n, false));
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++; // 岛屿数量 +1
dfs(grid, i, j); // 启动沉岛战术,标记整块岛屿
}
}
}
return ret;
}
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);
}
}
}
};
四、 岛屿的最大面积:带着战利品归来
4.1 题目描述
题目链接 :695. 岛屿的最大面积
描述 :
网格中
1代表土地,计算并返回网格中最大的岛屿面积(连通的1的数量)。如果没有岛屿,返回0。
4.2 深度拆解:全局变量的累加
这题和"岛屿数量"几乎一模一样。区别在于我们不但要消灭岛屿,还要在消灭的过程中数一数这块岛屿到底有几个格子 。
我们在启动 DFS 时,将计数器 count 置为 0。每走到一个陆地格子,count++。DFS 结束后,用 count 去更新全局最大值 ret。
4.3 C++ 代码实战
cpp
class Solution {
private:
bool vis[51][51] = {false};
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);
}
}
}
};
五、 被围绕的区域:正难则反的智慧
5.1 题目描述
题目链接 :130. 被围绕的区域
描述 :
找到所有被
'X'围绕的区域,并将这些区域里所有的'O'用'X'填充。注意:任何边界上的
'O'都不会被填充为'X',与边界上的'O'相连的'O'也不会被填充。
5.2 深度拆解:边缘渗透法
如果直接在网格内部找被包围的 'O',极其困难,因为你不知道这个连通块走到底会不会碰到边界。
正难则反:
-
那些逃不掉的
'O'必然是不挨着边界的。 -
那些能活下来的
'O',一定是有亲戚在边界上! -
我们只要沿着四个边界 走一圈,凡是碰到
'O',就启动 FloodFill,把这伙"皇亲国戚"全部打上保护标记(比如先染成.)。 -
最后遍历全盘:
- 没打保护标记的
'O'全是平民,统统变成'X'(被包围了)。 - 打了保护标记的
.恢复成'O'。
- 没打保护标记的
5.3 C++ 代码实战
cpp
class Solution {
private:
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. 扫描左右两边边界,寻找皇亲国戚
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 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);
}
// 3. 收网:清算平民,恢复皇亲
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];
int y = j + dy[k];
// 只去保护相邻的 'O'
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'O') {
dfs(board, x, y);
}
}
}
};
六、 太平洋大西洋水流问题:逆流而上的多源搜索
6.1 题目描述
题目链接 :417. 太平洋大西洋水流问题
描述 :
左边界和上边界是太平洋(Pacific),右边界和下边界是大西洋(Atlantic)。
水只能从高处流向低处或同等高度。
找到那些水流既能到达太平洋,又能到达大西洋的坐标。
6.2 深度拆解:逆向思维登峰造极
如果从每个点出发去模拟水流,看看能不能流到两大海,这会造成巨量的重复计算。
逆向思维(多源 DFS) :
水往低处流,反过来想就是海洋往高处爬!
- 让太平洋的海水从左上边界出发,能爬到的地方打上
pac标记(条件是新高度 >= 原始高度)。 - 让大西洋的海水从右下边界出发,能爬到的地方打上
atl标记。 - 最后全图遍历,既有
pac标记又有atl标记的格子,就是答案。
6.3 C++ 代码实战
cpp
class Solution {
private:
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, false));
vector<vector<bool>> atl(m, vector<bool>(n, false));
// 1. 太平洋 (左边和上边) 逆流而上
for (int i = 0; i < m; i++) dfs(h, i, 0, pac);
for (int j = 0; j < n; j++) dfs(h, 0, j, pac);
// 2. 大西洋 (右边和下边) 逆流而上
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);
// 3. 提取交集
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;
}
// 逆流而上的 DFS
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];
int 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);
}
}
}
};
七、 扫雷游戏:八方向的极限拉扯
7.1 题目描述
题目链接 :529. 扫雷游戏
描述 :
经典的扫雷规则。给你一个点击坐标
click,返回点击后的面板。如果点到雷
M,直接变X游戏结束。如果点到空块
E,统计周围 8 个方向雷的个数:
- 如果有雷,变成数字
1-8并停止蔓延。- 如果没有雷,变成
B,并向周围 8 个方向递归揭开相连的空块。
7.2 深度拆解:规则驱动的 FloodFill
这是一道纯粹的模拟题。关键在于:
- 方向数组要写 8 个!
- 只有当周围雷数为 0 时,水流(DFS)才会继续向外扩张。
7.3 C++ 代码实战
cpp
class Solution {
private:
// 上下左右 + 四个对角线
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) {
// 1. 统计周围 8 个方向的雷数
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++;
}
}
// 2. 根据雷数做决定
if (count > 0) {
// 如果周围有雷,当前格子变成雷数,并且停止蔓延
board[i][j] = count + '0';
} else {
// 如果周围没雷,当前格子变成 'B',并向八个方向蔓延
board[i][j] = 'B';
for (int k = 0; k < 8; k++) {
int x = i + dx[k], y = j + dy[k];
// 蔓延的条件:不越界,且必须是没有挖出的空块 'E'
if (x >= 0 && x < m && y >= 0 && y < n && board[x][y] == 'E') {
dfs(board, x, y);
}
}
}
}
};
八、 机器人的运动范围:带条件的 FloodFill
8.1 题目描述
题目链接 :LCR 130. 衣橱整理(原:机器人的运动范围)
描述 :
地上有一个
m行n列的方格,从坐标[0,0]开始移动。机器人不能进入行坐标和列坐标的数位之和大于
k的格子。问机器人能够到达多少个格子?
8.2 深度拆解:连通性的陷阱
为什么这题必须用 DFS(FloodFill)而不能直接用两个 for 循环遍历全图去判断数位和?
因为题目问的是"机器人能够到达 的格子"。
有些格子虽然数位和小于 k,但它被大于 k 的格子包围了(也就是不连通 ),机器人根本走不过去!
所以,必须从 (0, 0) 出发,老老实实地蔓延,走到的地方计数器 +1。
8.3 C++ 代码实战
cpp
class Solution {
private:
int m, n, k;
bool vis[101][101] = {false};
int ret = 0;
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
public:
int movingCount(int _m, int _n, int _k) {
m = _m; n = _n; k = _k;
dfs(0, 0);
return ret;
}
void dfs(int i, int j) {
// 到达合法格子,计数器加 1
ret++;
vis[i][j] = true;
for (int k = 0; k < 4; k++) {
int x = i + dx[k], y = j + dy[k];
// 条件:不越界 && 没走过 && 数位和 <= k
if (x >= 0 && x < m && y >= 0 && y < n && !vis[x][y] && check(x, y)) {
dfs(x, y);
}
}
}
// 辅助函数:计算 x 和 y 的数位之和
bool check(int i, int j) {
int sum = 0;
while (i) {
sum += i % 10;
i /= 10;
}
while (j) {
sum += j % 10;
j /= 10;
}
return sum <= k;
}
};
九、 FloodFill 总结
💬 复盘:FloodFill 其实是最轻松的搜索,因为它不需要你"事后擦屁股"(恢复现场)。
只要把握住两点:
- 修改即标记 :遇到符合条件的点,直接把它的值改掉(比如变颜色,或者用
vis数组),防止死循环。 - 多源变单源:遇到需要从边界向内、或者从两头向中间找的问题,可以把所有的起点一起扔进去(循环调用 DFS),正难则反,豁然开朗。