算法——搜索(最短路径BFS与DFS)

前言

  • DFS 是线,BFS 是面
  • 一般来说在找最短路径的时候使用 BFS,其他时候还是 DFS 使用得多一些(主要是递归代码好写)
  • BFS空间复杂度高,DFS时间复杂度高。

1 BFS

  • 无权图的最短路径
  • 在程序实现 BFS 时需要考虑以下问题:
    队列:用来存储每一轮遍历得到的节点
    标记:对于遍历过的节点,应该将它标记,防止重复遍历
  • 学习资料

1.1 BFS模板

int BFS(Node start, Node target) {
    Queue<Node> q; // 核心数据结构
    Set<Node> 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<TreeNode> 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 打开转盘锁

在这里插入图片描述
分析

  • 可看成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<String> deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 记录已经穷举过的密码,防止走回头路
    Set<String> visited = new HashSet<>();
    Queue<String> 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的添加位置,不能重复添加。
这种方法可以极限测试会超时

1.4 最小基因变化

在这里插入图片描述

  • 典型BFS
  • 知道了终点,双向BFS也可以做。

1. 常规BFScode:

public class 最小基因变化433 {
//    bfs
//    输入:start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
//    输出:2
    public int minMutation(String start, String end, String[] bank) {
        char[] all = {'A', 'C', 'G', 'T'};
//        存储路径字符串
        Set<String> road = new HashSet<>();
        for(String i:bank){
            road.add(i);
        }

//        输入数据判断
        if(start.equals(end)){
            return 1;
        }
        if(!road.contains(end)){
            return -1;
        }
//        队列
        Queue<String> que = new LinkedList<>();
//        历史节点 防止回头
        Set<String> visit = new HashSet<>();
        que.add(start);
        visit.add(start);
//        只要开始就默认找了一层
        int step = 1;
        while(!que.isEmpty()){
//            遍历一层
            int size = que.size();
            for(int i = 0;i < size;i++){
                String poll = que.poll();
                for(int j =0;j <8;j++){
                    for(int k =0 ;k <all.length;k++){
                        if(poll.charAt(j)!=all[k] ){
                            StringBuilder a = new StringBuilder(poll);
                            a.setCharAt(j,all[k]);

                            if(road.contains(a.toString()) && !visit.contains(a.toString())){
                                if(a.toString().equals(end)){
                                    return step;
                                }
                                que.add(a.toString());
                                visit.add(a.toString());
                            }
                        }

                    }

                }
            }
//            遍历一层,层数加一
            step ++;
        }
//        最后返回,肯定是没找到
        return -1;

    }

}

2. 双向BFS做法

    public int minMutation(String start, String end, String[] bank) {
        char[] all = {'A', 'C', 'G', 'T'};
//        存储路径字符串
        Set<String> road = new HashSet<>();
        for(String i:bank){
            road.add(i);
        }

//        输入数据判断
        if(start.equals(end)){
            return 1;
        }
        if(!road.contains(end)){
            return -1;
        }
//        队列
        Queue<String> que1 = new LinkedList<>();
        Queue<String> que2 = new LinkedList<>();
//        历史节点 防止回头
        Set<String> visit = new HashSet<>();

        que1.add(start);
        que2.add(end);
        visit.add(start);
//        只要开始就默认找了一层
        int step = 1;
//        que1一直来做bfs,que2记录相遇的节点
        while(!que1.isEmpty() && !que2.isEmpty()){
//            遍历一层
            int size = que1.size();
//            记录que1 接下来的一层
            Queue<String> temp = new LinkedList<>();
            for(int i = 0;i < size;i++){
                String poll = que1.poll();
                for(int j =0;j <8;j++){
                    for(int k =0 ;k <all.length;k++){
                        if(poll.charAt(j)!=all[k] ){
                            StringBuilder a = new StringBuilder(poll);
                            a.setCharAt(j,all[k]);
                            //相遇点在于 que2是否包含目标字符串
                            if(que2.contains(a.toString())){
                                return step;
                            }
                            if(road.contains(a.toString()) && !visit.contains(a.toString())){
                                temp.add(a.toString());
                                visit.add(a.toString());
                            }
                        }

                    }

                }
            }
//            遍历一层,层数加一
            step ++;

            que1 = que2;
            que2 =temp;
        }
//        最后返回,肯定是没找到
        return -1;

    }

2 双向BFS

  • 刚才讨论的二叉树最小高度的问题,你一开始根本就不知道终点在哪里,也就无法使用双向 BFS;但是第二个密码锁的问题,是可以使用双向 BFS 算法来提高效率的,代码稍加修改即可
  • start结点和target结点的来回切换扫描,通过temp临时结点来切换两个结点,同时判断相交的触发条件
  • visit集合做备忘录防止重复遍历,和deadends集合一样作用,排除无用结点。
  • 双向的含义,一个结点去遍历下一层,一个结点存储上一次遍历的一层结点,做相遇的触发条件。

思想手绘图
在这里插入图片描述

int openLock(String[] deadends, String target) {
    Set<String> deads = new HashSet<>();
    for (String s : deadends) deads.add(s);
    // 用集合不用队列,可以快速判断元素是否存在
    Set<String> q1 = new HashSet<>();
    Set<String> q2 = new HashSet<>();
    Set<String> visited = new HashSet<>();

    int step = 0;
    q1.add("0000");
    q2.add(target);

    while (!q1.isEmpty() && !q2.isEmpty()) {
        // 哈希集合在遍历的过程中不能修改,用 temp 存储扩散结果
        Set<String> 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<String> dead = new HashSet<>();
//        for(String Str:deadends){
//            dead.add(Str);
//        }
        //历史集合+死亡集合
        HashSet<String> history = new HashSet<>();
        for(String i:deadends){
            history.add(i);
        }
        //遍历节点队列
        LinkedList<String> que = new LinkedList<>();
        LinkedList<String> que2  = new LinkedList<>();
        que.offer("0000");
        que2.offer(target);


        //寻找的层数
        int step = 0;

        while(!que.isEmpty() && !que2.isEmpty()){
            LinkedList<String> 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<grid.length;i++)
    		for(int j=0;j<grid[0].length;j++)
    		{
    			if(grid[i][j]!='1') continue;
    			else dfs(grid,i,j);
    			count++;			
    		}
    	return count;
    }

}

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 填海造陆问题

问题

分析

  • 两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子

代码

3.4 岛屿的周长

问题
分析
代码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zkFun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值