当前位置: 首页 > news >正文

递归专题5 - FloodFill算法专题

递归专题5 - FloodFill算法专题

本文是递归算法系列的第5篇,完整体系包括:

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型
  4. 网格DFS与回溯
  5. 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 733Easy填充一个区域基础模板
LeetCode 200Medium计数区域数量全局遍历+计数
LeetCode 695Medium求最大区域面积void+全局变量
LeetCode 130Medium反向标记从边界出发
LeetCode 417Medium双边界+交集两个vis数组
LeetCode 529Medium8方向+条件递归统计周围状态

应用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);}
};

关键点:

  1. 特判 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;
  1. 判断条件是 != 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);}
};

常见错误:

  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,所以判断永远不成立
  1. 忘记花括号导致逻辑错误
// 错误
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);}
};

关键点:

  1. 每次进入新岛屿前重置tmp = 0
  2. 每个格子都累加tmp++
  3. 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);}
};

关键点:

  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);    // 下边界
}
  1. 用临时标记’#’

为什么不直接改成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);}}}
};

关键点:

  1. 两个vis数组
vector<vector<bool>> pac(m, vector<bool>(n));  // 太平洋
vector<vector<bool>> atl(m, vector<bool>(n));  // 大西洋
  1. 反向流动条件
// 正常流动:h[当前] >= h[下一个]
// 反向流动:h[下一个] >= h[当前]
if (h[x][y] >= h[i][j]) dfs(...);
  1. 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);}}}
};

关键点:

  1. 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};
  1. int转char
int count = 3;
board[i][j] = count + '0';  // 3 + 48 = 51 = '3'
  1. 条件递归
if (count > 0) {board[i][j] = count + '0';return;  // 有地雷,停止
} else {board[i][j] = 'B';// 只有没地雷才继续递归for (8个方向) dfs(...);
}

这是FloodFill的特殊变体:不是无条件递归,而是根据周围状态决定是否递归。


四、FloodFill总结

4.1 FloodFill vs 网格DFS

特性网格DFSFloodFill
目标找路径/最优解填充/标记区域
需要回溯
vis恢复需要不需要
修改原数组一般不改直接修改
典型题目单词搜索、黄金矿工岛屿问题

核心理解:

  • 网格DFS:需要尝试多条路径 → 要回溯
  • FloodFill:只走一次,填充后不回头 → 不需要回溯

4.2 FloodFill的六种应用

应用核心技巧
图像渲染基础模板,注意特判
岛屿数量全局遍历+计数
岛屿面积void+全局变量
被围绕区域反向思维,从边界出发
太平洋大西洋双边界,两个vis,取交集
扫雷游戏8方向,条件递归

4.3 常见错误总结

  1. FloodFill误用回溯
// 错误:FloodFill不需要恢复
image[i][j] = color;
dfs(...);
image[i][j] = oldColor;  // 不需要!
  1. 字符vs整数比较
// 错误
if (grid[i][j] == 0)  // grid是char类型// 正确
if (grid[i][j] == '0')  // 用字符'0'
  1. 忘记花括号
// 错误
if (...)dfs(...);ret++;  // 不在if内// 正确
if (...) {dfs(...);ret++;
}
  1. 初始色==目标色时的无限递归
// 必须特判
if (image[sr][sc] == color) return image;
  1. 数组越界 (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的简化版,核心特点是"不需要回溯":

为什么不需要回溯:

  • 目的是填充,不是搜索路径
  • 填充后不需要再访问
  • 修改本身就是标记

六种应用:

  1. 基础填充 (图像渲染)
  2. 计数区域 (岛屿数量)
  3. 求最大面积 (void+全局变量)
  4. 反向标记 (被围绕区域)
  5. 双边界+交集 (太平洋大西洋)
  6. 8方向+条件递归 (扫雷游戏)

核心技巧:

  • 直接修改原数组
  • 不需要vis数组 (或不需要恢复)
  • 反向思维 (从边界出发)
  • dx/dy数组优化代码
  • 4方向 vs 8方向

掌握这些模式和技巧,FloodFill的题基本就能快速解决。


系列文章

  1. 递归基础与思维方法
  2. 二叉树DFS专题
  3. 回溯算法十大类型
  4. 网格DFS与回溯
  5. FloodFill算法专题 (本文)

http://www.dtcms.com/a/553406.html

相关文章:

  • 系统架构设计师论文-论软件架构的复用
  • 沙市做网站weiswordwordpress微信登录设置
  • 理解MySQL的原理
  • Mac通过命令行开启ssh服务
  • 哈尔滨有哪些做网站的公司站长工具seo综合查询问题
  • 珠海做网站的wordpress 写作
  • 【计算机基础】之核心架构
  • 临西网站建设公司公司核名查询官网
  • PPIO独家上新GPU实例模板,一键部署Kimi-Linear
  • 工业级电池健康管理利器:GRX-3000 系列电池诊断站技术解析
  • 旅游网站建设功能意义wordpress 模板 免费
  • 周口市住房和城市建设局网站自做网站打开速度慢
  • STM32H743-ARM例程35-DHCP
  • 概率论直觉(一):大数定律
  • 数据结构—栈和队列
  • JavaSE知识分享——继承(下)
  • Linux性能分析:常用工具与指令
  • 软件测试面试的排序算法问题如何回答
  • Verilog和FPGA的自学笔记8——按键消抖与模块化设计
  • 深入解析 display: flow-root:现代CSS布局的隐藏利器
  • 汕头网站制作方法购物网站价格
  • 电商网站建设精准扶贫的目的建筑施工特种证书查询入口官网
  • spring-ai advisors 使用与源码分析
  • 关键词解释:点积(Dot Product)在深度学习中的意义
  • 本地部署DeepSeek-OCR:打造高效的PDF文字识别服务
  • 机器视觉系统中工业相机的常用术语解读
  • 【论文精读】GenRec:基于扩散模型统一视频生成与识别任务
  • seo提高网站排名wordpress内容页不显示
  • Velero(原名Heptio Ark) 是一个专为 Kubernetes 设计的开源备份恢复工具
  • 企业网站模板中文 产品列表深圳福田区住房和建设局网站