前言
上篇外面学习了深度优先搜索。今天我们来学习广度优先搜索(BFS)
BFS的核心思想:由远及近,层层扩展 ,按距离顺序遍历所有可达状态。
今天的例题,主要帮助大家来掌握:队列实现BFS的标准写法,理解入队出队,和访问标记的时机,能用BFS来解决最短路径,层级遍历,扩散类搜索等问题
200:岛屿数量
题目要求:给定一个 m×n 的二维网格 grid
'1' 表示陆地
'0' 表示水
统计岛屿数量
岛屿是由相邻的 '1' 组成(上下左右相连)
岛屿之间不相连
核心思路
遍历整个网格
- 遇到1;新岛屿
- 岛屿数量 count++
使用BFS遍历当前岛屿的所有陆地格子 - 将1入队
- 将访问过的格子标记为0,避免重复访问
- 堆队列中的格子,向四个方向扩展
- 直到队列空;当前岛屿BFS完成
重复上述两个操作遍历完所有格子,统计岛屿数量
代码实现
java
class Solution {
public int numIslands(char[][] grid) {
int rows=grid.length;
int cols=grid[0].length;
int count=0;
int[][] dirs= {{-1,0},{1,0},{0,-1},{0,1}}; // 上下左右
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(grid[i][j]=='1'){
count++;
grid[i][j]='0';
Queue<int[]> queue=new LinkedList<>();
queue.offer(new int[]{i,j});
while(!queue.isEmpty()){
int[] cell=queue.poll();
int r=cell[0];
int c=cell[1];
for(int[] d:dirs){
int nr=r+d[0];
int nc=c+d[1];
if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&grid[nr][nc]=='1'){
queue.offer(new int[]{nr,nc});
grid[nr][nc]='0';//标记访问
}
}
}
}
}
}
return count;
}
}
总结
兄弟们可以发现这道题出现在DFS中,现在他又出现在BFS中了。
因为本质上来说DFS和BFS都是一种搜索策略。两种搜索思路都能实现题目的解答。
我们来通过这道题来分析两种搜索的实现有哪些不一样。
我们先来看相同的地方
java
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(grid[i][j]=='1'){
count++;
grid[i][j]='0';
这一块实现的功能都是先通过遍历找到一地块陆地。只有在找到陆地之后,我们才能开展不同的搜索策略。于是区别就来了。
我们先来看BFS:
java
while(!queue.isEmpty()){
int[] cell=queue.poll();
int r=cell[0];
int c=cell[1];
for(int[] d:dirs){
int nr=r+d[0];
int nc=c+d[1];
if(nr>=0&&nr<rows&&nc>=0&&nc<cols&&grid[nr][nc]=='1'){
queue.offer(new int[]{nr,nc});
grid[nr][nc]='0';//标记访问
BFS事先维护了一个队列queue(存放第一块陆地)。因为BFS是像病毒扩散一样从母体一步步向外扩散。其中的母体就是队列中存放的陆地坐标i,j。扩散方式就是将母体坐标进行上下左右变换。最后通过判断,是就感染。不是直接跳过
再来看DFS:
java
private void dfs(char[][] grid,int i,int j){
int rows=grid.length;
int cols=grid[0].length;
//边界判断+遇到水就停
if(i<0||i>=rows||j<0||j>=cols||grid[i][j]=='0'){
return;
}
//把当前陆地变为水
grid[i][j]='0';
//向四个方向扩散
dfs(grid, i + 1, j); // 下
dfs(grid, i - 1, j); // 上
dfs(grid, i, j + 1); // 右
dfs(grid, i, j - 1); // 左
}
DFS采用了递归/回溯的方法。扩散方式是先深度走一个分支,如果扩散完了,通过回溯的方法,回到分支的岔路口,走另一个岔路。
695:岛屿的最大面积
题目要求:给定一个 m×n 的二维网格 grid:
'1' 表示陆地
'0' 表示水
找出最大岛屿的面积
岛屿由相邻的 '1' 组成(上下左右相连)
返回面积最大的岛屿格子数量
核心思路
遍历整个网格
- 遇到1;新岛屿
- BFS计算该岛面积
BFS遍历岛屿 - 将格子(i,j)入队
- 标记访问过
- 队列非空时:出队格子(r,c)
- 面积累加
- 四个方向都检查
更新最大面积 maxArea = Math.max(maxArea, curArea)
代码实现
java
import java.util.LinkedList;
import java.util.Queue;
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
int maxArea = 0;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}}; // 上下左右
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == 1) {
int curArea = 0;
Queue<int[]> queue = new LinkedList<>();
queue.offer(new int[]{i, j});
grid[i][j] = 0; // 标记访问
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int r = cell[0], c = cell[1];
curArea++; // 累加面积
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols && grid[nr][nc] == 1) {
queue.offer(new int[]{nr, nc});
grid[nr][nc] = 0; // 标记访问
}
}
}
maxArea = Math.max(maxArea, curArea);
}
}
}
return maxArea;
}
}
总结
本题与上一题类似,都是有BFS和DFS两种解法。
在这里不再做两种搜索策略的区分,与上题大同小异
本题额外多的点是需要额外维护一个面积字段。根据遍历的推进进行累加。
注意点:面积累加的位置放在了while下而不是最后一个if中。
标准做法:curArea=0 +出队累加
994:腐烂的橘子
题目要求:
给定一个 m×n 的二维网格 grid:
0 → 空格子
1 → 新鲜橘子
2 → 腐烂橘子
每分钟,腐烂橘子会让 上下左右相邻的新鲜橘子腐烂
返回 使所有橘子腐烂所需的最少分钟数
如果无法全部腐烂,返回 -1
核心思路
本题天然适合BFS(层次遍历)且BFS层次可以直接表示分钟数
具体流程:
初始化队列
- 所有腐烂橘子入队
- 统计心思按橘子数量freshCount
BFS逐层扩展 - 队列非空:
- 遍历当前层所有腐烂橘子
- 将相邻的新鲜橘子腐烂-入队
- freshCount--
- 分钟数 minutes++
结束条件 - BFS完成
- 如果freshCount==0;返回minutes
- 否则;返回-1
代码实现
java
class Solution {
public int orangesRotting(int[][] grid) {
int rows=grid.length;
int cols=grid[0].length;
Queue<int[]> queue=new LinkedList<>();
int freshCount=0;
//初始化
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
if(grid[i][j]==2){
queue.offer(new int[]{i,j});
}else if(grid[i][j]==1){
freshCount++;
}
}
}
if(freshCount==0) return 0;//没有新鲜橘子
int minutes=0;
int[][] dirs = {{-1,0},{1,0},{0,-1},{0,1}}; // 上下左右
while(!queue.isEmpty()){
int size=queue.size();
boolean rottenThisRound=false;
for(int i=0;i<size;i++){
int[] cell=queue.poll();
int r=cell[0], c=cell[1];
for(int[] d:dirs){
int nr=r+d[0], nc=c+d[1];
if(nr >= 0 && nr < rows && nc >= 0 && nc < cols &&grid[nr][nc]==1){
grid[nr][nc]=2;
queue.offer(new int[]{nr,nc});
freshCount--;
rottenThisRound=true;
}
}
}
if(rottenThisRound) minutes++;//只有再这一轮有新鲜橘子腐烂时增加分钟
}
return freshCount==0?minutes:-1;
}
}
总结
这道题是典型的BFS题目。BFS搜索就像感染一样层层递进
对于本道题我们从相同点与不同点进行理解。
相同点:
首段for循环遍历来寻找腐烂橘子并入队
最后BFS遍历方式大同小异
不同点:
在全代码中,一些需要额外维护的变量贯穿始终、
freshCount记录新鲜橘子数量,用于判断最终能否将所有橘子感染
minutes是我们最终需要返回的结果;分钟数
布尔类型的rottenThisRound;用来辅助判断minutes什么适合需要累加
相同点在前面的练习难度不大,主要是不同点。
对于不同点来说,因为题目都是字段。考察我们对于字段的运用时机(代码中的位置)是关键
比如rottenThisRound辅助minutes;我曾疑惑为什么不直接将minutes放在最后一个for循环遍历(因为最后一个循环是检查当前队列元素的四个方向相邻格子,还并没有确认感染)
最后来看,将前面两道掌握好,这题的注意点就只有如下:
注意点:
额外字段的位置