leetcode刷题|???算法——搜索(最短路径BFS与DFS)


文章目录

  • 前言
  • 1 BFS
    • 1.1 BFS模板
    • 1.2 二叉树的最小深度
    • 1.3 打开转盘锁
  • 2 双向BFS
  • 3 DFS
    • 3.1 岛屿的数量
    • 3.2 岛屿的最大面积
    • 3.3 填海造陆问题
    • 3.4 岛屿的周长

前言
  • DFS 是线,BFS 是面
  • 一般来说在找最短路径的时候使用 BFS,其他时候还是 DFS 使用得多一些(主要是递归代码好写)
  • BFS空间复杂度高,DFS时间复杂度高。
1 BFS
  • 无权图的最短路径
  • 在程序实现 BFS 时需要考虑以下问题:
    队列:用来存储每一轮遍历得到的节点
    标记:对于遍历过的节点,应该将它标记,防止重复遍历
  • 学习资料
1.1 BFS模板
int BFS(Node start, Node target) { Queue q; // 核心数据结构 Set visited; // 避免走回头路q.offer(start); // 将起点加入队列 visited.add(start); int step = 0; // 记录扩散的步数while (q not empty) { int sz = q.size(); /* 将当前队列中的所有节点向四周扩散 */ for (int i = 0; i < sz; i++) { Node cur = q.poll(); /* 划重点:这里判断是否到达终点 */ if (cur is target) return step; /* 将 cur 的相邻节点加入队列 */ for (Node x : cur.adj()) if (x not in visited) { q.offer(x); visited.add(x); } } /* 划重点:更新步数在这里 */ step++; } }

1.2 二叉树的最小深度 BFS
分析:更改终点情况;以为是子树不用考虑历史结点问题
int minDepth(TreeNode root) { if (root == null) return 0; Queue q = new LinkedList<>(); q.offer(root); // root 本身就是一层,depth 初始化为 1 int depth = 1; while (!q.isEmpty()) { int sz = q.size(); /* 将当前队列中的所有节点向四周扩散 */ for (int i = 0; i < sz; i++) { TreeNode cur = q.poll(); /* 判断是否到达终点 */ if (cur.left == null && cur.right == null) return depth; /* 将 cur 的相邻节点加入队列 */ if (cur.left != null) q.offer(cur.left); if (cur.right != null) q.offer(cur.right); } /* 这里增加步数 */ depth++; } return depth; }

DFS
public int minDepth(TreeNode root) { if (root == null) return 0; int left = minDepth(root.left); int right = minDepth(root.right); if (left == 0 || right == 0) return left + right + 1; return Math.min(left, right) + 1; }

1.3 打开转盘锁 leetcode刷题|???算法——搜索(最短路径BFS与DFS)
文章图片

分析
  • 可看成8叉树,四位每一位有两个分支
  • 八个结果看作一层,相当于一个操作数,step+1;
  • 终止条件是结果==target,肯定是最短的先触发,最短路径。
  • 字符串比较记得用equels。大坑
  • == 基本类型比较的是数值相等,包装类比较是的是否同一个对象,,重写equels的类可能会比较数值。
代码
//往上拨 String plusOne(String s, int j) { char[] ch = s.toCharArray(); if (ch[j] == '9') ch[j] = '0'; else ch[j] += 1; return new String(ch); } // 将 s[i] 向下拨动一次 String minusOne(String s, int j) { char[] ch = s.toCharArray(); if (ch[j] == '0') ch[j] = '9'; else ch[j] -= 1; return new String(ch); }int openLock(String[] deadends, String target) { // 记录需要跳过的死亡密码 Set> deads = new HashSet<>(); for (String s : deadends) deads.add(s); // 记录已经穷举过的密码,防止走回头路 Set> visited = new HashSet<>(); Queue> q = new LinkedList<>(); // 从起点开始启动广度优先搜索 int step = 0; q.offer("0000"); visited.add("0000"); while (!q.isEmpty()) { int sz = q.size(); /* 将当前队列中的所有节点向周围扩散 */ for (int i = 0; i < sz; i++) { String cur = q.poll(); /* 判断是否到达终点 */ if (deads.contains(cur)) continue; if (cur.equals(target)) return step; /* 将一个节点的未遍历相邻节点加入队列 */ for (int j = 0; j < 4; j++) { //每个结点两种操作 String up = plusOne(cur, j); if (!visited.contains(up)) { q.offer(up); visited.add(up); } String down = minusOne(cur, j); if (!visited.contains(down)) { q.offer(down); visited.add(down); } } } /* 在这里增加步数 */ step++; } // 如果穷举完都没找到目标密码,那就是找不到了 return -1; }

优化
可以不需要dead这个哈希集合,可以直接将这些元素初始化到visited集合中,要注意visit的添加位置,不能重复添加。
这种方法可以极限测试会超时
2 双向BFS
  • 刚才讨论的二叉树最小高度的问题,你一开始根本就不知道终点在哪里,也就无法使用双向 BFS;但是第二个密码锁的问题,是可以使用双向 BFS 算法来提高效率的,代码稍加修改即可
  • start结点和target结点的来回切换扫描,通过temp临时结点来切换两个结点,同时判断相交的触发条件
  • visit集合做备忘录防止重复遍历,和deadends集合一样作用,排除无用结点。
  • 双向的含义,一个结点去遍历下一层,一个结点存储上一次遍历的一层结点,做相遇的触发条件。
思想手绘图
leetcode刷题|???算法——搜索(最短路径BFS与DFS)
文章图片

int openLock(String[] deadends, String target) { Set> deads = new HashSet<>(); for (String s : deadends) deads.add(s); // 用集合不用队列,可以快速判断元素是否存在 Set> q1 = new HashSet<>(); Set> q2 = new HashSet<>(); Set> visited = new HashSet<>(); int step = 0; q1.add("0000"); q2.add(target); while (!q1.isEmpty() && !q2.isEmpty()) { // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果 Set> temp = new HashSet<>(); /* 将 q1 中的所有节点向周围扩散 */ for (String cur : q1) { /* 判断是否到达终点 */ if (deads.contains(cur)) continue; if (q2.contains(cur)) return step; visited.add(cur); /* 将一个节点的未遍历相邻节点加入集合 */ for (int j = 0; j < 4; j++) { String up = plusOne(cur, j); if (!visited.contains(up)) temp.add(up); String down = minusOne(cur, j); if (!visited.contains(down)) temp.add(down); } } /* 在这里增加步数 */ step++; // temp 相当于 q1 // 这里交换 q1 q2,下一轮 while 就是扩散 q2 q1 = q2; q2 = temp; } return -1; }

leetdoe752双向BFS解法
package com.zknode.BFS_and_DFS; import java.util.HashSet; import java.util.LinkedList; public class ti_752 { //单个字符的加操作 public String pulsStr(String str,int j){ char[] chars = str.toCharArray(); if(chars[j] =='9'){ chars[j] = '0'; }else { chars[j] += 1; } return new String(chars); } public String minusStr(String str, int j){ char[] chars = str.toCharArray(); if(chars[j] == '0'){ chars[j] = '9'; }else{ chars[j]-=1; } return new String(chars); }public int openLock(String[] deadends, String target) { //死亡集合 //HashSet dead = new HashSet<>(); //for(String Str:deadends){ //dead.add(Str); //} //历史集合+死亡集合 HashSet> history = new HashSet<>(); for(String i:deadends){ history.add(i); } //遍历节点队列 LinkedList> que = new LinkedList<>(); LinkedList> que2= new LinkedList<>(); que.offer("0000"); que2.offer(target); //寻找的层数 int step = 0; while(!que.isEmpty() && !que2.isEmpty()){ LinkedList> temp = new LinkedList<>(); //BFS的一层 for(String node:que){ //历史路径查询+死亡结点查询 ,不考虑该路径 if(history.contains(node)){ continue; }if(que2.contains(node)){ return step; }//统一添加历史结点位置,在出队列后加,也可以在入队列里加 history.add(node); //四个字母分别动两下 for(int j =0 ; j<4 ; j++){ String plusnum = pulsStr(node,j); //历史路径查询+死亡结点查询 ,不考虑该路径 if(!history.contains(plusnum)){ temp.offer(plusnum); } String minusnum = minusStr(node,j); if(!history.contains(minusnum)){ temp.offer(minusnum); } } } step++; que = que2; que2 = temp; } return -1; } public static void main(String[] args) { ti_752 ti_752 = new ti_752(); String[] str = new String[]{"0201","0101","0102","1212","2002"}; System.out.println(ti_752.openLock(str,"0202")); } }

3 DFS
  • DFS算法的核心:做选择再递归,回溯算法就是DFS的一种表现形式
网格DFS框架模板
void dfs(int[][] grid, int r, int c) { // 判断 base case if (!inArea(grid, r, c)) { return; } // 如果这个格子不是岛屿,直接返回 if (grid[r][c] != 1) { return; } grid[r][c] = 2; // 将格子标记为「已遍历过」// 访问上、下、左、右四个相邻结点 dfs(grid, r - 1, c); dfs(grid, r + 1, c); dfs(grid, r, c - 1); dfs(grid, r, c + 1); }// 判断坐标 (r, c) 是否在网格中 boolean inArea(int[][] grid, int r, int c) { return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length; }

3.1 岛屿的数量 详情
问题
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
输入:grid =
[ [“1”,“1”,“1”,“1”,“0”],
[“1”,“1”,“0”,“1”,“0”],
[“1”,“1”,“0”,“0”,“0”],
[“0”,“0”,“0”,“0”,“0”] ]
输出:1
分析
  • 岛屿问题最常用的就是DFS。当然也可以使用BFS和并查集。
代码
class Solution { public void dfs(char[][] grid,int i,int j) { int nr = grid.length; int nc = grid[0].length; if(i>nr-1 || j>nc-1 ||i<0||j<0) return; if(grid[i][j]!='1') return; grid[i][j]='2'; dfs(grid,i+1,j); dfs(grid,i,j+1); dfs(grid,i,j-1); dfs(grid,i-1,j); } public int numIslands(char[][] grid) { int count=0; if(grid.length==0 || grid ==null) return 0; for(int i=0; i

3.2 岛屿的最大面积 问题
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。
分析
  • 依次扫描为1的网格,扫到之后DFS计算面积。
代码
public int maxAreaOfIsland(int[][] grid) { int res = 0; for (int r = 0; r < grid.length; r++) { for (int c = 0; c < grid[0].length; c++) { if (grid[r][c] == 1) { int a = area(grid, r, c); res = Math.max(res, a); } } } return res; }int area(int[][] grid, int r, int c) { if (!inArea(grid, r, c)) { return 0; } if (grid[r][c] != 1) { return 0; } grid[r][c] = 2; return 1 + area(grid, r - 1, c) + area(grid, r + 1, c) + area(grid, r, c - 1) + area(grid, r, c + 1); }boolean inArea(int[][] grid, int r, int c) { return 0 <= r && r < grid.length && 0 <= c && c < grid[0].length; }

3.3 填海造陆问题 问题
  • leetcode827
【leetcode刷题|???算法——搜索(最短路径BFS与DFS)】分析
  • 两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子
代码
3.4 岛屿的周长 问题
分析
代码

    推荐阅读