我是一个从汽车行业转行IT的项目经理,我是Edward。今天我们来聊一下,dfs和bfs,连着两天leetcode每日一题给到dfs,着实让我领略到了dfs查找功能的强大,那么我们先一起来看看这两道题。
22. 括号生成
数字 n 代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的 括号组合。
示例:
输入:n = 3
输出:[
“((()))”,
“(()())”,
“(())()”,
“()(())”,
“()()()”
];
package algorithm;
/**
* 强大的dfs查找
* @author EP
* <p>22. 括号生成<p>
* @date 2020年4月9日
* @version 1.0
*/
import java.util.ArrayList;
import java.util.List;
public class Solution_generateParenthesis {
public static void main(String[] args) {
int n = 5;
res = generateParenthesis(n);
for (String string : res) {
System.out.println(string);
}
}
static List<String>res = new ArrayList<String>();
public static List<String> generateParenthesis(int n) {
search(n, n, "");
return res;
}
private static void search(int left,int right,String curString) {
if (left==0&&right==0) { //递归终止的base case,左右都不剩即终止
res.add(curString);
return;
}
if (left>0) { //如果左括号还剩余,可以拼接左括号
search(left-1, right, curString+"(");
}
if (right>left) { //如果右括号剩余多于左括号剩余,可以拼接右括号
search(left, right-1, curString+")");
}
}
}
可以看到,只要设定了规则,dfs就可以不断递归帮你完成深度查找。
面试题13. 机器人的运动范围
地上有一个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。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
提示:
1 <= n,m <= 100 0 <= k <= 20
public class Solution_movingCount {
public static void main(String[] args) {
int m=2,n=3,k=1;
System.out.println(movingCount(m, n, k));
}
static boolean[][]visited;
public static int movingCount(int m, int n, int k) {
//设置boolean数组标记访问状态
visited = new boolean[m][n];
//设置递归查找方法
return search(0, 0, m, n, k);
}
//据说是dfs
private static int search(int x,int y,int m,int n,int k) {
//设置递归终止的base case
if (x>=m||y>=n||visited[x][y]||(x%10+x/10+y%10+y/10)>k) {
return 0;
}
//其他情况则标记、+1、继续找
visited[x][y]=true;
return 1+search(x+1, y, m, n, k)+search(x, y+1, m, n, k);
}
}
这道面试题中也涉及到了dfs查找需要注意的一点,就是设置标记,记录访问状态。
再来看看bfs的场景:1162. 地图分析
- 你现在手里有一份大小为 N x N 的『地图』(网格) grid,上面的每个『区域』(单元格)都用 0 和 1 标
- 记好了。其中 0 代表海洋,1 代表陆地,你知道距离陆地区域最远的海洋区域是是哪一个吗?请返回该海
- 洋区域到离它最近的陆地区域的距离。
我们这里说的距离是『曼哈顿距离』( Manhattan Distance):(x0, y0) 和 (x1, y1) 这两个
区域之间的距离是 |x0 - x1| + |y0 - y1| 。
如果我们的地图上只有陆地或者海洋,请返回 -1。
示例 1:
输入:[[1,0,1],[0,0,0],[1,0,1]] 输出:2 解释: 海洋区域 (1, 1) 和所有陆地区域之间的距离都达到最大,最大距离为 2。
提示:
1 <= grid.length == grid[0].length <= 100
grid[i][j] 不是 0 就是 1
public class Solution_maxDistance {
public static void main(String[] args) {
int[][]grid={{1,0,1},{0,0,0},{1,0,1}};
System.out.println(maxDistance(grid));
}
public static int maxDistance(int[][] grid) {
int[]dx= {0,0,1,-1};
int[]dy= {1,-1,0,0}; //建立四个方向
Queue<int[]>queue=new ArrayDeque<>(); //建立新队列
int m =grid.length, n=grid[0].length;
//先把所有陆地入队
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j]==1) {
queue.offer(new int[] {i,j});
}
}
}
//从各个陆地开始,一圈一圈地遍历海洋,最后遍历到的海洋就是离陆地最远的海洋
boolean hasOcean=false;
int[]point=null;
while (!queue.isEmpty()) {
point =queue.poll(); //建立空数列接收queue里面返回的陆地
int x = point[0], y=point[1];
//将取出队列的元素的四周的海洋入队
for (int i = 0; i < 4; i++) {
int newX = x+dx[i];
int newY = y+dy[i];
if (newX<0||newX>=m||newY<0||newY>=n||grid[newX][newY]!=0) { //当超出范围或者不为海洋时跳过
continue;
}
//在原数组的基础上进行了更新,对访问过的海洋进行了标记
grid[newX][newY]=grid[x][y]+1;
hasOcean = true;
queue.offer(new int[] {newX,newY}); //将新找到的海洋入队
}
}
//没有陆地或者没有海洋,返回-1
if (!hasOcean||point==null) {
return -1;
}
//返回最后一次遍历到的海洋的距离
return grid[point[0]][point[1]]-1;
}
}
可以看到bfs主要的适用场景是判断最大或最小路径以及可达性分析,以前不太理解为什么图解算法上要把队列和bfs放在一起,这下算明白了,就像是派遣舰队,依次把舰只派遣出去搜寻未知的土地,然后再将搜到的资源依次归队,再次出队。
再来看看这道题,是4月15日的打卡题,咋看之下跟地图分析非常类似:
542. 01 矩阵
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例 1:
输入:
0 0 0
0 1 0
0 0 0
输出:
0 0 0
0 1 0
0 0 0
示例 2:
输入:
0 0 0
0 1 0
1 1 1
输出:
0 0 0
0 1 0
1 2 1
注意:
给定矩阵的元素个数不超过 10000。
给定矩阵中至少有一个元素是 0。
矩阵中的元素只在四个方向上相邻: 上、下、左、右。
//努力按照maxDistance的思路改了一下还是失败了,看看sweetie改的
// 首先将所有的 0 都入队,并且将 1 的位置设置成 -1,表示该位置是 未被访问过的 1
Queue<int[]> queue = new LinkedList<>();
int m = matrix.length, n = matrix[0].length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) {
queue.offer(new int[] {i, j});
} else {
matrix[i][j] = -1;
//(设成Integer.MAX_VALUE啦,m * n啦,10000啦都行,只要是个无效的距离值来标志这个位置的 1 没有被访问过就行辣~)
}
}
}
int[] dx = new int[] {-1, 1, 0, 0};
int[] dy = new int[] {0, 0, -1, 1};
while (!queue.isEmpty()) {
int[] point = queue.poll();
int x = point[0], y = point[1];
for (int i = 0; i < 4; i++) {
int newX = x + dx[i];
int newY = y + dy[i];
// 如果四邻域中的某点是 -1,则表示该点是未被访问过的 1
// 所以这个点到 0 的距离就可以更新成 matrix[x][y] + 1
// 然后将该点入队继续查找
// 这样每个点就只会被入队一次
if (newX >= 0 && newX < m && newY >= 0 && newY < n
&& matrix[newX][newY] == -1) {
matrix[newX][newY] = matrix[x][y] + 1;
queue.offer(new int[] {newX, newY});
}
}
}
return matrix;
自己实现起来并不容易,我尝试在地图分析源码上进行修改,但是失败了,总结一下几个关键点:
1:先入队的元素如何选.虽然每个点都要入队,但是先入队的点决定了后续逻辑,二选一的话,最好选本身值不变的点,bfs就可以直接更新四周的值.
2:标记未访问过的点,原则上一个点只入队一次.比如这题,未访问过的点已为1,就先初始化为-1,访问过再变为1入队.
这里也推荐一个大佬,甜姨,她每天都会在半夜半小时之内完成leetcode打卡,她的公众号【甜姨的奇妙冒险】和 知乎专栏【甜姨的力扣题解】,希望更多人领略到算法之美。