LeetCode - DFS模板秒杀题
1. DFS基本框架
图中的DFS
核心问题就是 "遍历二维数组"
二维数组遍历框架如下:
void dfs (int[][] grid , int i , int j , boolean[] visited) {
int m = gird.length , n = grid[0].length;
//检查是否越界
if (i < 0 || j < 0 || i >= m || j >= n) {
//超出索引边界
return ;
}
//检查该点是否已经访问过 或者 排除题目中无关的结点
if (visited[i][j] || grid[i][j] != ... ) {
return ;
}
//访问结点
visited[i][j] = true;
//进行“感染”
grid[i][j] = ... ;
dfs(grid,i+1,j,visited); //上
dfs(grid,i-1,j,visited); //下
dfs(grid,i,j-1,visited); //左
dfs(grid,i,j+1,visited); //右
}
//小tips: 可以使用"方向数组"来实现上下左右的遍历
int[][] dirs = new int[][]{ {-1,0} , {1,0} , {0,-1} , {0,1} };
//递归遍历上下左右结点
for (int[] d : dirs) {
int next_i = i + d[0];
int next_j = j + d[1];
dfs(grid , next_i , next_j , visited);
}
树中的DFS
相比图中简单许多,不需要判断是否重复访问
int dfs (TreeNode root , ...) {
//检查是否到头
if (root == null) {
return ...;
}
//检查左右孩子的一些特殊情况
if (root.left ... && root.right ... ) {
return ...;
}
//访问结点
更新相关变量
//进行“感染” 只要传入左右孩子即可
return ...
dfs(root.left,...);
dfs(root.right,...
}
2. LeetCode 相关例题
(1)Flood Fill算法(岛屿问题)
Flood 是「洪水」的意思,Flood Fill 直译是「泛洪填充」的意思 体现了洪水能够从一点开始,迅速填满当前位置附近的地势低的区域。
类似的应用还有:PS 软件中的「点一下把这一片区域的颜色都替换掉」,扫雷游戏「点一下打开一大片没有雷的区域」该类题目一般都可以使用 【DFS BFS 并查集 】来解决 DFS: 通过DFS定义一个"淹岛/感染"函数
LC 733 图像渲染(油漆桶工具)
LeetCode 733 要实现的功能就类似于PS 中的"油漆桶工具"
直接按照模板来即可
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
//如果需要调整的颜色 没变 则直接返回
if(image[sr][sc]==newColor)
return image;
dfs(image,sr,sc,image[sr][sc],newColor);
return image;
}
//dfs
public void dfs(int[][] image,int x,int y,int oldColor,int newColor){
//坐标越界和不同色的情况
if(x<0 || x>=image.length || y<0 || y>=image[0].length || image[x][y]!=oldColor){
return ;
}
image[x][y] = newColor; //上色
dfs(image,x+1,y,oldColor,newColor);
dfs(image,x-1,y,oldColor,newColor);
dfs(image,x,y+1,oldColor,newColor);
dfs(image,x,y-1,oldColor,newColor);
}
}
LC 200 岛屿数量
可以和"染色"联系到一起 那个将某一块(某一个岛屿)进行染色 而该题是求有多少个块(多少个岛屿)? 该题中DFS的作用是?
依旧是"染色" 不过准确的说是"把岛淹了",避免重复计数
先遍历找,找到一块区域为1(陆地)就计数+1,然后调用DFS把该岛淹了
class Solution {
public int numIslands(char[][] grid) {
int res = 0; //岛屿的数量
for (int i = 0; i < grid.length;i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == '1') { //发现一个
res++;
dfs(grid,i,j); //把这个岛淹了
}
}
}
return res;
}
//用dfs把这个岛淹了
public void dfs (char[][] grid , int i , int j) {
//检测是否越界
if (i < 0 || j < 0 || i >= grid.length || j >= grid[0].length)
return;
//检测是否到出了该岛屿
if (grid[i][j] != '1')
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);
}
}
LC 695 岛屿的最大面积
和上一题也类似
只不过是要在淹岛的时候顺便求一下面积
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int res = 0; //记录最大面积
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
res = Math.max(res,dfs(grid,i,j));
}
}
}
return res;
}
//淹岛的同时返回该岛的面积
public int dfs (int[][] grid , int i, int j) {
//检测边界值
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length)
return 0;
//出了岛屿的边界
if (grid[i][j] == 0)
return 0;
//淹了
grid[i][j] = 0;
return dfs(grid,i-1,j)
+ dfs(grid,i+1,j)
+ dfs(grid,i,j+1)
+ dfs(grid,i,j-1) + 1;
}
}
LC 1020 飞地的数量
LeetCode 1020
本质上就是求 “封闭的岛屿” 需要那种四周都被水围着"真正意义上的岛屿"
就是将四周(上下左右)的岛先淹了,再去统计剩下的即可
//将四周的淹了
for (int j = 0; j < grid[0].length; j++) {
dfs(grid,0,j); //淹靠上的
dfs(grid,grid.length-1,j); //淹靠下的
}
for (int i = 0; i < grid.length; i++) {
dfs(grid,i,0); //淹靠左的
dfs(grid,i,grid[0].length-1); //淹靠右的
}
之后就正常操作 如果要求"封闭岛屿"的数量,就一个一个淹岛
如果要求"封闭岛屿"的面积,就一边淹岛一边比大小
该题中求的是所有"封闭岛屿"的总面积,就不需要淹岛了,直接遍历即可
完整代码如下:
class Solution {
public int numEnclaves(int[][] grid) {
int res = 0;
//先将四边的岛屿淹了
for (int j = 0; j < grid[0].length; j++) {
dfs(grid,0,j); //淹靠上的
dfs(grid,grid.length-1,j); //淹靠下的
}
for (int i = 0; i < grid.length; i++) {
dfs(grid,i,0); //淹靠左的
dfs(grid,i,grid[0].length-1); //淹靠右的
}
//统计剩余的岛屿数量
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if (grid[i][j] == 1) {
res++;
//dfs(grid,i,j);
}
}
}
return res;
}
public void dfs(int[][] grid, int i, int j) {
//检测是否越界
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length)
return;
//检测是否到边界
if (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);
}
}
LC 1034 边界着色
LeetCode 1034
先理解题意:
连通分量:指的是上下左右四个方向中某个方向挨着,并且颜色相同的格子的集合(就是挨着的格子)
边界着色:row、col这个坐标指向的连通分量中挨着其他颜色的格子或者挨着矩阵边界的格子称为边界,将这些边界着色。
和DFS淹没岛屿的思路一致 :
连通分量 相当于 岛屿
给连通分量着色 相当于淹没岛屿 (题中要求的是将连通分量的边界着色)基本思路: 一样是淹没岛屿的思想 (1) 先将连通分量整体都着色 (2) 再次遍历,把内部元素都恢复为原来的颜色 就达到边界着色的效果了 怎么判断是否是内部元素? 周边四方元素都被访问过了 if (visited[i][j] && visited[i+1][j] && visited[i-1][j] && visited[i][j-1] && visited[i][j+1]) {...}
class Solution {
public:
vector<vector<int>> colorBorder(vector<vector<int>>& grid, int row, int col, int color) {
if (grid[row][col] == color)
return grid;
int oldColor = grid[row][col];
vector<vector<bool>> visited(grid.size(),vector(grid[0].size(),false));
dfs(grid,row,col,oldColor,color,visited); //先把连通分量找出来
//在连通分量中找出边界值
for (int i = 1; i < grid.size() - 1; i++) {
for (int j = 1; j < grid[0].size() - 1; j++) {
// 寻找内部点 把内部点给染回去
if (visited[i][j] && visited[i+1][j] && visited[i-1][j] && visited[i][j-1] && visited[i][j+1]) {
grid[i][j] = oldColor;
}
}
}
return grid;
}
//基本思路:
//第一轮: dfs 类似于岛屿 把连通分量淹下去
//第二轮: 再次遍历 在其中找出内部点并染回去
void dfs(vector<vector<int>>& grid , int i , int j , int oldColor , int newColor,vector<vector<bool>>& visited) {
//边界值判断
if (i < 0 || i >= grid.size() || j < 0 || j >= grid[0].size())
return ;
//判断是不是到了边界
if (grid[i][j] != oldColor)
return ;
//四方向寻找 “连通分量的规则”
visited[i][j] = true; //标记一下被淹了的连通分量
grid[i][j] = newColor;
dfs(grid,i+1,j,oldColor,newColor,visited);
dfs(grid,i-1,j,oldColor,newColor,visited);
dfs(grid,i,j+1,oldColor,newColor,visited);
dfs(grid,i,j-1,oldColor,newColor,visited);
}
};
(2)搜索应用
剑指Offer 13 机器人的运动范围
class Solution {
public int movingCount(int m, int n, int k) {
boolean[][] visited = new boolean[m][n];
return dfs(0,0,m,n,k,visited);
}
public int dfs(int i,int j,int m,int n,int k,boolean[][] visited) {
//超边界
if (i < 0 || i >= m || j < 0 || j >= n) {
return 0;
}
//超过k 以及已被访问 (防止走回头路)
if (i%10 + i/10 + j/10 + j%10 > k || visited[i][j]) {
return 0;
}
//标记
visited[i][j] = true;
//扩散到周围结点
return 1+dfs(i+1,j,m,n,k,visited)
+dfs(i-1,j,m,n,k,visited)
+dfs(i,j+1,m,n,k,visited)
+dfs(i,j-1,m,n,k,visited);
}
}
LC 1306 跳跃游戏III
// 每次向外扩散 只会两种 start+arr[start] start-arr[start]
class Solution {
public boolean canReach(int[] arr, int start) {
boolean[] visited = new boolean[arr.length];
return dfs(arr,start,visited);
}
public boolean dfs(int[] arr, int start,boolean[] visited) {
//越界 以及 已被访问 (避免走回头路)
if (start < 0 || start >= arr.length || visited[start]) {
return false;
}
int step = arr[start];
//标记一下
visited[start] = true;
//成功到达
if (step == 0) {
return true;
}
return dfs(arr,start+step,visited) || dfs(arr,start - step,visited);
}
}
(3)二叉树相关的DFS
LC112 路径总和
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
return dfs(root,targetSum);
}
public boolean dfs (TreeNode root , int targetSum) {
if (root == null) {
return false;
}
if (root.left == null && root.right == null) {
return root.val == targetSum;
}
return dfs(root.left , targetSum - root.val)
|| dfs(root.right , targetSum - root.val);
}
}
LC129 求根节点到叶子结点数字之和
class Solution {
public int sumNumbers(TreeNode root) {
return dfs(root , 0);
}
int dfs (TreeNode root , int sum) {
if (root == null)
return 0;
if (root.left == null && root.right == null)
return sum + root.val;
sum += root.val;
return dfs(root.left , 10*sum)
+ dfs(root.right , 10*sum);
}
}