目录
floodfill算法概念:
floodfill算法是一种常用的图像处理算法,用于填充连通区域。它从指定的种子点开始,将相邻的像素点按照某种条件进行填充,直到所有符合条件的像素点都被填充为止。floodfill算法常用于图像处理中的填充、边界提取、区域分割等任务。它可以用于实现画图工具中的油漆桶工具,也可以用于图像分割中的区域填充。
floodfill算法的中文名为洪水灌溉算法,基本实现内容:从某个点开始向四周扩散,直到无法再扩散为止。(可以使用DFS和BFS,本文讲解DFS解决)从这个实现内容我们不难发现这很适合我们利用爆搜来实现(一般数据范围不会太大)。如下图数字为1的就是一个性质相同的连通块。这类问题通常会问我们1的最大联通块是多大?或者有几个1的连通块?使用floodfill算法解决的题目基本一眼就能看得出来,不想滑动窗口和动态规划之类的算法,你不试一试是看不出来要使用什么算法的😭😭😭。
算法模板套路:
创建所需的全局变量(最好设置为静态,因为非静态只有在leetcode上才行)
static boolean[][] vis;//( 不一定要有)
static int[ ] dx = {0 , 0 , 1 , -1 };
static int[ ] dy = {1 , -1 , 0 , 0 };
vis这个布尔类型数组来标记我们已经走过的路,防止重复走导致死循环。如果题目可以改给出的二维数组的值的话可以修改值就不用创建这个数组来标记了。
dx,dy数组配合使用,对应就是上下左右走,这是四个方向(上下左右)的,如果题目要求8个方向的话可以在草稿纸上画个图推一下很快的。
其他的一些条件根据题目的具体情况创建。
DFS模板如下:
其中n为行的个数,m为列的个数,至于要不要回溯要根据题目要求什么来决定。如果是8个方向的话把改一下dx和dy还有k的值变为8即可。x >= 0 && x < n && y >= 0 && y < m 这个可以说是默写了,因为这就是防止越界,每道题目都是这么写的。
public static void dfs(int i,int j,.....){
for(int k = 0;k < 4;k++){
int x = i + dx[k];
int y = j + dy[k];
if(x >=0 && x < n && y >= 0 && y < m && !vis[x][y] && 题目条件){
vis[x][y] = true;
dfs(x,y,.....);
vis[x][y] = false;//回溯
}
}
}
上面的这些是作者自己总结出来的,友友们刚开始可以试试,后面做的熟练了基本写出这些就是肌肉记忆了,这个在比赛中也会考到。
下面给出的例题前三题必看,可以说是floodfill算法的经典题型了。
例题1:图像渲染
有一幅以 m x n
的二维整数数组表示的图画 image
,其中 image[i][j]
表示该图画的像素值大小。
你也被给予三个整数 sr
, sc
和 newColor
。你应该从像素 image[sr][sc]
开始对图像进行 上色填充 。
为了完成 上色工作 ,从初始像素开始,记录初始坐标的 上下左右四个方向上 像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应 四个方向上 像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为 newColor
。
最后返回 经过上色渲染后的图像 。
详细题解:
可以利用深搜,遍历到与该点相连的所有像素相同的点,然后将其修改成指定的像素即可。
这里再强调一下,在leetcode中不加static是可以的,但是在比赛中(要求自己写Main类)只能用static修饰的。
直接对题目给出的一个点进行DFS,本题是直接在二位数组image的基础上直接修改那么就不用用到 vis 来标记了,因为走过的地方值被修改了那么就不会出现重复走的情况。具体代码如下:
class Solution {
int[] dx = {0,0,1,-1};
int[] dy = {1,-1,0,0};
int prev;
int m,n;
public int[][] floodFill(int[][] image, int sr, int sc, int color) {
if(image[sr][sc] == color){
return image;
}
prev = image[sr][sc];
m = image.length;
n = image[0].length;
dfs(image,sr,sc,color);
return image;
}
public void dfs(int[][] image,int i,int j,int color){
image[i][j] = color;
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);
}
}
}
}
例题2:岛屿数量
给你一个由 '1'
(陆地)和 '0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
详细题解:
非常经典的一道floodfill算法题目其思路过程为:
遍历整个矩阵,每当找到一块陆地时,岛屿数量 + 1,并且将这个陆地相连的所有陆地,也就是这块岛屿,全部变成海洋。这样的话,我们下次遍历到这块岛屿的时候,它已经是海洋了,不会影响最终结果(当然也可以不修改矩阵,我们可以使用vis数组来完成去重操作)。变成海洋的操作可以利用DFS解决和上面给出的模板差不多😎😎😎
代码实现:
class Solution {
static int ret;
static boolean[][] vis;
static int n;
static int m;
static int[] dx = {0,0,1,-1};
static int[] dy = {1,-1,0,0};
public static int numIslands(char[][] grid) {
ret = 0;
n = grid.length;
m = grid[0].length;
vis = new boolean[n][m];
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
if(grid[i][j] == '1' && !vis[i][j]){
dfs(grid,i,j);
ret++;
}
}
}
return ret;
}
public static void dfs(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 < n && y >= 0 && y < m && grid[x][y] == '1' && !vis[x][y]){
dfs(grid,x,y);
}
}
}
}
例题3:岛屿的最大面积
给你一个大小为 m x n
的二进制矩阵 grid
。
岛屿 是由一些相邻的 1
(代表土地) 构成的组合,这里的「相邻」要求两个 1
必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid
的四个边缘都被 0
(代表水)包围着。
岛屿的面积是岛上值为 1
的单元格的数目。
计算并返回 grid
中最大的岛屿面积。如果没有岛屿,则返回面积为 0
。
没错又是这张图因为比较经典且作者水平有限😭😭😭,所以就直接使用了。
详细题解:
遍历整个矩阵,每当遇到⼀块土地的时候,就用DFS将与这块土地相连的整个岛屿的面积计算出来。然后在搜索得到的所有岛屿面积中求一个最大值即可。这题我们就可以使用vis数组来标记这个位置是否已经被访问过了。
求这种最大值的又是深搜,建议大家自己设置一个变量来专门存储临时值和最大值,我一般存储临时值采用path,存储结果也就是最大值采用ret,step表示递归的层数(这个floodfill算法不涉及,在别的DFS题会用到)这个都行就是自己要明白。
class Solution {
int[] dx = {0,0,1,-1};
int[] dy = {1,-1,0,0};
int ret;
boolean[][] vis;
int n,m;
int path;
public int maxAreaOfIsland(int[][] grid) {
n = grid.length;
m = grid[0].length;
vis = new boolean[n][m];
ret = 0;
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
if(grid[i][j] == 1 && !vis[i][j]){
path = 0;
dfs(grid,i,j);
ret = Math.max(ret,path);
}
}
}
return ret;
}
public void dfs(int[][] grid,int i,int j){
vis[i][j] = true;
path++;
for(int k = 0;k < 4;k++){
int x = i + dx[k];
int y = j + dy[k];
if(x >= 0 && x < n && y >= 0 && y < m &&
grid[x][y] == 1 && !vis[x][y]){
dfs(grid,x,y);
}
}
}
}
相信做完上面三道题目,友友们是不是觉得有点没有意思,感觉就是在考背诵一样,DFS的写法基本差不多,上面的只要根据题意稍加修改即可。其实DFS确实差不多,有些难点是在如何使用DFS而不是写出DFS,例如下面的例题四,运用到了我们解决问题的经典思路正难则反。
例题4:被围绕的区域
给你一个 m x n
的矩阵 board
,由若干字符 'X'
和 'O'
,找到所有被 'X'
围绕的区域,并将这些区域里所有的 'O'
用 'X'
填充。
可以看到如果我们不加以转换的话直接按照我们前面三题的思路来的话有点困难,因为要判断很多边界情况,且要进行两遍DFS,所以我们要想一种更为巧妙的解法:正难则反。
解题过程:
可以先利用dfs将与边缘相连的'0' 区域做上标记,然后重新遍历矩阵,将没有标记过的'0' 修改成'X' 即可。就是这么的简单。遍历边缘就画个图模拟一下即可,至于DFS的编写那还不是手到擒来🤩🤩🤩 ,具体代码如下:
class Solution {
int[] dx = {0,0,1,-1};
int[] dy = {1,-1,0,0};
boolean[][] vis;
int n,m;
public void solve(char[][] board) {
n = board.length;
m = board[0].length;
vis = new boolean[n][m];
//标记vis
for(int i = 0;i < n;i++){
if(board[i][0] == 'O'){
dfs(board,i,0);
}
if(board[i][m - 1] == 'O'){
dfs(board,i,m - 1);
}
}
for(int i = 0;i < m;i++){
if(board[0][i] == 'O'){
dfs(board,0,i);
}
if(board[n - 1][i] == 'O'){
dfs(board,n - 1,i);
}
}
//将O用X填充
for(int i = 0;i < n;i++){
for(int j = 0;j < m;j++){
if(board[i][j] == 'O' && !vis[i][j]){
board[i][j] = 'X';
}
}
}
}
public void dfs(char[][] board,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 < n && y >= 0 && y < m && board[x][y] == 'O' &&
!vis[x][y]){
dfs(board,x,y);
}
}
}
}
总结:这类题难点在如何使用DFS,全局变量要根据具体情况来创建,这些搞定后写个DFS就是默写的事情。
结语:
其实写博客不仅仅是为了教大家,同时这也有利于我巩固知识点,和做一个学习的总结,由于作者水平有限,对文章有任何问题还请指出,非常感谢。如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。