总言
主要内容:编程题举例,熟悉理解floodfill类题型如何用深搜或广搜的方法解决。
文章目录
1、floodfill洪水灌溉
FloodFill(洪水灌溉)问题通常指的是一种区域填充算法。这种算法被用来填充图像中颜色或亮度值相同的连通区域。
FloodFill算法(也被称为漫水填充法)通常是这样一个过程:给定一个起始点(称为种子点),该点的颜色或亮度值已知,算法会搜索并填充与种子点相连通且颜色或亮度值相同的所有像素点。 这个“连通”通常定义为四连通(上、下、左、右)或八连通(上、下、左、右、左上、右上、左下、右下)。
在计算机视觉和图像处理中,FloodFill算法有广泛的应用,如图像分割、边缘检测、色彩填充等。在图像分割中,它可以帮助我们将图像分割成颜色或亮度值不同的区域;在边缘检测中,它可以用来识别出图像中颜色或亮度值变化快速的位置;在色彩填充中,它可以将指定区域的颜色替换为新的颜色。
2、图像渲染(medium)
题源:链接。
2.1、DFS
1)、思路分析
此题即洪水灌溉问题,要求找一篇连通块,将其值修改。可以使用DFS和BFS两种策略,遍历到与该点相连的所有「像素相同的点」。
这里我们先使用深索解决:从给定的起点开始,向其四周(四连通)进行DFS遍历。每次搜索到一个方格时,若其与初始位置的方格颜色相同,就将该方格的颜色更新,以防止重复搜索;若不相同,则进行回溯。
注意:因为初始位置的颜色会被修改,所以我们需要保存初始位置的颜色,以便于之后的更新操作。
2)、题解
class Solution {
int newcolor;
int oldcolor;
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
// 若原始的像素值和需要填充的像素值相同, 直接返回(不用做修改)
if (color == image[sr][sc])
return image;
newcolor = color;
oldcolor = image[sr][sc];
DFS(image, sr, sc);// 深搜,递归找值修改
return image;
}
// 上(x-1, y)、下(x+1, y)、左(x,y-1)、右(x,y+1)
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void DFS(vector<vector<int>>& image, int sr, int sc) {
// 修改当前方格
image[sr][sc] = newcolor;
// 递归寻找四周方格
for(int i = 0; i < 4; ++i)
{
int x = dx[i] + sr;
int y = dy[i] + sc;
if(x >= 0 && x < image.size() && y >=0 && y < image[0].size()
&& image[x][y] == oldcolor)
{
DFS(image, x, y);
}
}
}
};
2.2、BFS
1)、思路分析
BFS的思路和DFS相同,区别只是遍历的次序不同:从给定的起点开始,进行广度优先搜索。每次搜索到一个方格时,如果其与初始位置的方格颜色相同,就将该方格加入队列,并将该方格的颜色更新,以防止重复入队。
注意:因为初始位置的颜色会被修改,所以我们需要保存初始位置的颜色,以便于之后的更新操作。
2)、题解
class Solution {
typedef pair<int,int> PII;// 队列中存放的是矩阵下标
int dx[4] = {-1,1,0,0};
int dy[4] = {0,0,1,-1};
public:
vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) {
int oldcolor = image[sr][sc];// 原始像素值
if(oldcolor == color) return image;// 若原始的像素值和需要填充的像素值相同, 直接返回(不用做修改)
queue<PII> q;
q.push({sr,sc});// 将起始值所在下标放入队列中
while(!q.empty())// 队列不为空时
{
// 取队头元素:修改其像素值
auto [a,b] = q.front();
q.pop();
image[a][b] = color;
// 根据取出的元素下标,向其四周遍历,将合法的元素入队
for(int k = 0; k < 4; ++k)
{
int x = a + dx[k];
int y = b + dy[k];
if(x >= 0 && x < image.size() && y >= 0 && y < image[0].size()
&& image[x][y] == oldcolor)
q.push({x,y});
}
}
return image;
}
};
3、岛屿数量(medium)
题源:链接。
3.1、题解
1)、思路分析
实则和图像渲染的那题相同,区别在于那题给定了初始坐标点,本题需要自己找初始坐标点。此外,多一个返回值统计。
由于我们不知道岛屿数目,因此可以从头开始遍历矩阵,每当找到一块陆地时,以其为起始坐标,向四周扩散(BFS或DFS),寻找与之相连的陆地,直到无法扩散为止,这样我们就获取到了一块岛屿,将其计入统计中。
注意:我们向四周扩散时,有可能会重复回到原先已经统计过的陆地。为了避免重复,需要做一定处理: ①可以像图像渲染的那题一样,修改已经统计过的陆地坐标值(这种做法会破坏原始矩阵,要看题目是否允许使用,若是面试时,可以问问面试管是否允许);②或者使用一个标记数组对遍历过的坐标进行标记。
2)、题解
使用DFS:
class Solution {
vector<vector<bool>> visited;//标记数组,用于判断某一位置陆地是否已经被统计
int m,n;
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size();// 行
n = grid[0].size();// 列
visited = vector<vector<bool>>(m,vector<bool>(n,0));
int count = 0;
// 暴搜:以(i,j)为起始点,寻找岛屿
for(int i = 0; i < m; ++i)
{
for(int j =0; j < n; ++j)
{
// 判断当前(i,j)是否是一块未标记过的陆地
if(grid[i][j] == '1' && !visited[i][j])
{
count++;// 若是,说明其可以成为一个新的岛屿,统计。(此语句顺序不固定)
visited[i][j] = true;// 标记该陆地
DFS(grid,i,j);// 向其四周寻找
}
}
}
return count;//返回统计结果
}
int dx[4] = {-1, 1, 0 ,0};
int dy[4] = {0, 0, -1, 1};
void DFS(vector<vector<char>>& grid, int i, int j)
{
// 向四周寻找相连陆地
for(int k =0; k < 4; ++k)
{
int x = dx[k] + i;
int y = dy[k] + j;
if(x >= 0 && x < m && y >= 0 && y < n && !visited[x][y] && grid[x][y] == '1')
{
visited[x][y] = true;
DFS(grid,x,y);
}
}
}
};
使用BFS:
class Solution {
vector<vector<bool>> visited;// 标记数组:用于判断某一位置陆地是否已经被统计
int m,n;
public:
int numIslands(vector<vector<char>>& grid) {
m = grid.size(); n = grid[0].size();
visited = vector<vector<bool>>(m,vector<bool>(n));// 初始化
int count = 0;// 用于统计最终岛屿数量
// 从头开始遍历,以其为起始点向四周扩散找岛屿
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(!visited[i][j] && grid[i][j] == '1')// 若当前元素(i,j)是未被标记过的岛屿
{ //以其为起始点,进行宽搜
visited[i][j] = true;// 标记i,j(这里标记也可以在BFS里进行)
BFS(grid,i,j);
++count;
}
}
}
return count;
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void BFS(vector<vector<char>>& grid, int i, int j)
{
queue<pair<int,int>> q;// 队列:用于广度优先遍历(宽搜)
q.push({i,j});// 将给定的起始元素下标入队
while(!q.empty())
{
// 取队头元素
auto [a,b] = q.front();
q.pop();
// 向四周遍历,找满足条件的元素(下标),入队
for(int k = 0; k < 4; ++k)
{
int x = dx[k] + a;
int y = dy[k] + b;
if(x >= 0 && x < m && y >= 0 && y < n && !visited[x][y] && grid[x][y] == '1')
{
visited[x][y] = true;// 标记
q.push({x,y});
}
}
}
}
};
4、岛屿的最大面积(medium)
题源:链接。
4.1、题解
1)、思路分析
思路同之前的题,使用宽搜或广搜,每当遇到一块土地的时候,计入一次统计。如此,一次搜索结束,会获取到一块岛屿的面积。比较所有岛屿的面积,求最大值即可。
PS:为了避免重复,每经过一次土地,就需要将其标记(或者使用修改值的方法)
2)、题解
使用深搜的写法:
class Solution {
vector<vector<bool>> visited;// 标记数组
int m,n;
int maxcount = 0;
int count = 0;
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
m = grid.size(); n = grid[0].size();
visited = vector<vector<bool>>(m,vector<bool>(n,0));
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(grid[i][j] == 1 && !visited[i][j])
{
count = 0;
DFS(grid, i, j);
maxcount = max(maxcount, count);// 做一个比较获取最大岛屿值
}
}
}
return maxcount;
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = { 0, 0, -1, 1};
void DFS(vector<vector<int>>& grid, int i, int j)
{
visited[i][j] = true;// 对当前陆地块标记
++count;// 将当前陆地块统计进入本回合岛屿数目中
for(int k = 0; k < 4; k++)
{
// 获取当前陆地块四周的坐标
int x = dx[k] + i;
int y = dy[k] + j;
// 向四周探索
if(x >=0 && x < m && y >=0 && y < n && !visited[x][y] && grid[x][y] == 1)
{
DFS(grid,x,y);
}
}
}
};
使用宽搜的写法:注意,这里需要在入栈前就标记遍历过的岛屿下标,而非等到出栈时才做标记(后者会导致重复标记)。
class Solution {
bool visited[51][51];// 标记数组
int m,n;
int count;// 用于统计单个岛屿面积
public:
int maxAreaOfIsland(vector<vector<int>>& grid) {
m = grid.size(); n = grid[0].size();
int maxcount = 0;// 用于统计最大的岛屿面积(这里我们在获取到单个岛屿面积后才进行比较,故使用了局部变量)
// 从头遍历,定岛屿起始坐标
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(grid[i][j] && !visited[i][j])
{
count = 0;// 每一个新回合前,都需要初始化(或者使用局部遍历作为参数传入)
BFS(grid, i, j);
maxcount = max(maxcount, count);
}
}
}
return maxcount;
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void BFS(vector<vector<int>>& grid, int i, int j)
{
++count;// 统计当前(i,j)位置
visited[i][j] = true;// 标记
queue<pair<int,int>> q;// 用于进行宽搜的队列
q.push({i,j});// 将给定的起始坐标入队
while(q.size())
{
auto [a,b] = q.front();// 取队头元素
q.pop();
for(int k = 0; k < 4; ++k)// 向四周遍历找陆地
{
int x = a + dx[k];
int y = b + dy[k];
// 如果(x,y)满足要求,入队
if(x >= 0 && x < m && y >=0 && y < n && !visited[x][y] && grid[x][y])
{
q.push({x,y});// 将(x,y)入队
++count;// 统计当前(i,j)位置
visited[x][y] = true;// 标记
}
}
}
}
};
5、被围绕的区域(medium)
题源:链接。
5.1、题解
1)、思路分析
思路一:遍历矩阵,找元素为"O"的连通块,但需要注意,若与边相连,则非连通块,需要特殊处理。即,先找目标区域,再判断是否连通边界。这种方法虽然也能解决问题,但处理起来相对麻烦,因此我们可以放过来思考。
思路二:正难则反,先找边界连通块,再确定目标区域。我们可以先遍历矩阵四边,将与边缘相连的 “O” 区域做上标记(修改为其它值)。然后重新遍历矩阵,若遇到没有标记过的 “O”,将其修改成 “X”;若遇到被修改后的值,将其复原为原先的"O"。
细节:整体思想如上,至于如何遍历找值。可以使用BFS,也可以使用DFS。 下述将展示两种写法。
2)、题解
使用深度优先搜索实现标记操作:把边界的连通块 O
修改为.
(这里修改值任意,不与目标值X
同即可)。
class Solution {
int m,n;
public:
void solve(vector<vector<char>>& board) {
m = board.size();// 行数
n = board[0].size();// 列数
// 先遍历一遍边界的元素,处理存在边界上的连通块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, '.');
}
// 从头开始遍历
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(board[i][j] == 'O') board[i][j] = 'X';// 非边界的O,需要修改为X
else if(board[i][j] == '.') board[i][j] = 'O';//还原边界:不做修改
}
}
}
int dx[4] = {-1,1,0,0};
int dy[4] = {0,0,-1,1};
// DFS作用:以(i,j)位置起始,向四周找连通块,将其修改为ch值
void DFS(vector<vector<char>>& board, int i, int j, char ch)
{
// 修改(i,j)位置
char tmp = board[i][j];//记录一下原先位置
board[i][j] = ch;// 将其修改为一个不等于X或O的其余字符ch
// 向四周扩散修改
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 && board[x][y] == tmp)
{
DFS(board,x,y,ch);
}
}
}
};
使用广度优先搜索实现标记操作:
class Solution {
int m,n;
public:
void solve(vector<vector<char>>& board) {
m = board.size(); n = board[0].size();
// 遍历,找四个边界上是否存在O
for(int i = 0; i < m; ++i)//处理首列和尾列
{
if(board[i][0] == 'O') BFS(board, i, 0, '.');
if(board[i][n-1] == 'O') BFS(board, i, n-1,'.');
}
for(int j = 0; j < n; ++j)
{
if(board[0][j] == 'O') BFS(board, 0, j, '.');
if(board[m-1][j] == 'O') BFS(board, m-1, j, '.');
}
// 重头开始再次遍历
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(board[i][j] == 'O') board[i][j] = 'X';
if(board[i][j] == '.') board[i][j] = 'O';
}
}
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void BFS(vector<vector<char>>& board, int i, int j, char ch)
{
board[i][j] = ch;
queue<pair<int,int>> q;// 用于宽搜的队列
q.push({i,j});// 将给定的初始元素下标入队
while(q.size())
{
// 取队头元素
auto [a,b] = q.front();
q.pop();
// 向四周搜索,将符合条件的元素入队
for(int k = 0; k < 4; ++k)
{
int x = a + dx[k];
int y = b + dy[k];
if(x >= 0 && x < m && y >=0 && y < n && board[x][y] == 'O')
{
board[x][y] = ch;// 将满足条件的值修改为ch值
q.push({x,y});// 入队
}
}
}
}
};
6、太平洋大西洋水流问题(medium)
题源:链接。
6.1、题解
1)、思路分析
可以使用正难则反的思想。
直接去判断某⼀个位置是否既能到大西洋又到太平洋,会重复遍历很多路径,相对麻烦。
因此我们可以反着来,从大西洋沿岸(边界坐标)开始反向搜索(DFS或BFS) ,这样就能找出可以流向大西洋方格;同理,从太平洋沿岸也反向搜索 ,找出可以流向太平洋方格。那么,被二者同时标记过的方格,就是我们要找的结果。
2)、题解
class Solution {
vector<vector<bool>> pacific;// 标记是否能够流过太平洋
vector<vector<bool>> atlantic;// 标记是否能够流过大西洋
int m,n;
public:
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
m = heights.size();
n = heights[0].size();
pacific = vector<vector<bool>>(m,vector<bool>(n,false));
atlantic = vector<vector<bool>>(m,vector<bool>(n,false));
// 逆向思考,遍历边界,判断(上、左)、(下、右)两个洋的水能逆流到哪,对能够逆流的区域进行标记
for(int j = 0; j < n; ++j)
{
DFS(heights, 0, j,pacific);// 太平洋:上侧边界
DFS(heights, m-1, j,atlantic);// 大西洋:下侧边界
}
for(int i = 0; i < m; ++i)
{
DFS(heights,i,0,pacific);// 太平洋:左侧边界
DFS(heights,i,n-1,atlantic);// 大西洋:右侧边界
}
// 再次遍历,找同时被两个洋均标记的位置
vector<vector<int>> ret;
for(int i = 0; i < m; ++i)
{
for(int j = 0; j < n; ++j)
{
if(pacific[i][j] && atlantic[i][j])
ret.push_back({i,j});
}
}
return ret;
}
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void DFS(vector<vector<int>>& heights, int i, int j, vector<vector<bool>>& ocean)
{
ocean[i][j] = true;
for(int k = 0; k < 4; ++k)
{
int x = i + dx[k];
int y = j + dy[k];
//逆流判断:(i,j)-->(x,y),满足(i,j)位置的海拔不高于(x,y),则表示(i,j)能流向洋流ocean中,进行标记
if(x >= 0 && x < m && y >=0 && y < n && heights[i][j] <= heights[x][y] && !ocean[x][y])//ocean[x][y]表示当前点满足条但还未被标记过
{
DFS(heights, x, y, ocean);// 以(x,y)为扩散点,继续寻找四周流向
}
}
}
};
7、扫雷游戏(medium)
题源:链接。
7.1、题解
1)、思路分析
本题本质是一道模拟题。
2)、题解
class Solution {
int m, n;
public:
vector<vector<char>> updateBoard(vector<vector<char>>& board,vector<int>& click) {
// 从(x,y)位置点击扫雷
int x = click[0], y = click[1];
if (board[x][y] == 'M') // 上手直接点到雷
{
board[x][y] = 'X'; // 修改为 'X',游戏结束。
return board;
}
// 上手未点到雷,开始递归八个方位
m = board.size(); n = board[0].size();
DFS(board, x, y);
return board; // 将结果返回
}
// 上(x-1,y)、下(x+1,y)、左(x,y-1)、右(x,y+1)、
// 左上(x-1,y-1)、左下(x+1,y-1)、右上(x-1,y+1)、右下(x+1,y+1)
int dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1};
int dy[8] = {0, 0, -1, 1, -1, -1, 1, 1};
void DFS(vector<vector<char>>& board, int i, int j) {
// 判断当前(x,y)位置四周是否有雷
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') // 若当前(x,y)位置是未挖出的雷
++count; // 统计
}
// 根据四周统计情况,决定当前位置是继续递归,还是修改为数字
if (count > 0)
{
board[i][j] = count + '0';// 当前(i,j)位置周围有雷,将(i,j)位置标记成数字
return;
}
else {
board[i][j] = 'B';// 当前(i,j)位置周围没有雷,则将(i,j)位置标记为B,继续递归
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] == 'E')
DFS(board,x,y); // 递归
}
}
}
};
8、衣橱整理(medium)
题源:链接。
PS:此题同剑指offer题:机器人运动范围。当前题目信息未描述清楚,建议结合评论中的题目补充一并理解。
剑指原题目:
地上有一个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。请问该机器人能够到达多少个格子?
8.1、题解
1)、思路分析
如何判断格子合法?可以根据补充的题目信息,写一个digit函数,用于取下标位数和。
如何移动?可以从(0,0)
坐标开始,使用宽搜或深搜,向右侧或下侧移动,统计出所有合法的格子。
2)、题解
下述是使用DFS解题的写法:(也可以使用BFS)
class Solution {
int count;// 用于统计
bool visited[101][101] = {false};// 用于标记遍历过的位置
public:
int wardrobeFinishing(int m, int n, int cnt) {
DFS(m,n,cnt,0,0);
return count;
}
void DFS(int m, int n, int cnt, int i, int j)
{
// 判断当前(i,j)位置的digit(i)+digit(j)
int ret = digit(i) + digit(j);
visited[i][j] = true;
if(ret > cnt) return;
else count++;
// 向右、向下递归
if(j + 1 < n && !visited[i][j+1]) DFS(m,n,cnt,i,j+1);//向右递归
if(i + 1 < m && !visited[i+1][j]) DFS(m,n,cnt,i+1,j);//向下递归
}
int digit(int n)
{
int ret = 0;
while(n)
{
ret += n % 10;
n/=10;
}
return ret;
}
};