搜索算法:BFS/DFS问题

  2020-4-25 


  1. DFS BFS搜索算法经常带有一个visited矩阵

  2. DFS算法入口一般传入第一轮位置参数,就像树遍历传入root根节点一样,然后每一轮DFS(每一层递归)就调用DFS(参数位置+1)(即下一个位置,具体不一定要这样)

    其涵盖拥有前面递归所说的一切经验:理解子问题,思考齐全①递归出口(所有终止条件)、②递归入栈(如何继续递归)与③执行体(做什么操作)(①和②可以合并在一起,不符合条件不继续递归,自然出栈)

    经常需要使用全局变量来记录递归中的参数

    对于DFS,要有子问题思维:以前、以后的问题是不可见的,现在只需要考虑当前该节点的小问题,当前结点拥有哪些变量

    也要有全局思维:理清子问题与以前、以后问题的关系

    回溯返回值可以跨越递归层级进行各种信息(当然得当前结点看得见的)传递

  3. 对于BFS问题,有两个关键:出队的时候执行执行体,入队的时候判断是否入队(这两者可以在一起,即入队就是上层出队结点的执行体)

    队列为空则遍历结束

  4. DFS的关键:①入口与出口 ②【单线程执行】(进栈出栈顺序),回溯(同层多入口。同层多入口间要回溯以免同层其它递归路径的影响)

结点搜索问题:机器人的运动范围

核心问题:

遍历到符合要求的结点就全局计数,路径走不通就返回

注意是搜索符合要求的结点

(DFS)思路:维持一个全局计数矩阵,DFS中走到死路直接该条递归路径出口

(BFS)思路:在BFS中走到死路不入队


问题:

地上有一个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. DFS解法

    class Solution 
    {
        //标记矩阵
        boolean[][] visited;
        int count = 0;
    
        public int movingCount(int m, int n, int k)
        {
            visited = new boolean[m][n];
            //init visited array
            for(int i=0;i<m;i++)
                for(int j=0;j<n;j++)
                    visited[i][j] = false;
    
             //开始dfs
            dfs(0,0,m,n,k);
            return count;
        }
    
        public void dfs(int x,int y,int m,int n,int k)
        {
            //出口条件
            //注意边界条件,题目中写的是0~m-1,0~n-1走,不能走到n
            if(x>m-1 || y>n-1 || (x/10+x%10+y/10+y%10)>k || visited[x][y]==true)
                return ;
            //执行体
            count++;
            visited[x][y] = true;
            //递归入栈
            dfs(x,y+1,m,n,k);
            dfs(x+1,y,m,n,k);
        }
    }
  2. BFS解法

    class Solution 
    {
        boolean[][] visited;
        int count = 0;
    
        public int movingCount(int m, int n, int k)
        {
            visited = new boolean[m][n];
            //init visited array
            for(int i=0;i<m;i++)
                for(int j=0;j<n;j++)
                    visited[i][j] = false;
    
             //开始bfs;注意这里将坐标以一个数组的形式记录入队
            Queue<Integer[]> qe = new LinkedList();
            qe.add(new Integer[]{0,0});
    
            while(!qe.isEmpty())
            {
                //出栈,执行体
                Integer[] pos = qe.remove();
                //【这里判断本层结点是否执行执行体:计数count+1】
                if ((pos[0]/10+pos[0]%10+pos[1]/10+pos[1]%10)<=k && visited[pos[0]][pos[1]]==false)
                {
                    count++;
                    visited[pos[0]][pos[1]] = true;
                    //【本层结点能执行说明路没被堵,这才判断相邻下一层结点是否入队;所以要放进外层if执行体里】
                    if (pos[0]<m && pos[1]+1<n)
                        qe.add(new Integer[]{pos[0],pos[1]+1});
                    if (pos[0]+1<m && pos[1]<n)
                        qe.add(new Integer[]{pos[0]+1,pos[1]});
                }
            }
            return count;
        }
    }

    本题DFS比BFS快13倍

字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。

你可以以任意顺序返回这个字符串数组,但里面不能有重复元素

示例:

输入:s = "abc"
输出:["abc","acb","bac","bca","cab","cba"]

解答:

import java.util.*;
class Solution {
    List<String> res = new LinkedList<>();
    //用来缓存得到的搜索路径
    char[] c;
    String[] permutation(String s)
    {
        //转化为字符数组
        c = s.toCharArray();
        dfs(0);
        return res.toArray(new String[0]);  //注意,传入的这个参数表明array元素的类型,必须是一个对象
    }

    //每次递归的路径该怎么记录:因为【用的是交换来固定元素以确定路径】当一堆交换,那么字符串本身就是结果!

    void dfs(int x)
    {
        //dfs到最后一位了,这时候已经通过交换确定了一个搜索路径了。所以即为c本身(一个序列)
        if (x==c.length-1)
        {
            res.add(String.valueOf(c));
            return ;
        }

        HashSet<Character> set = new HashSet<>();
        for(int i = x; i < c.length; i++)
        {
            //使用hashset判断要交换的i位是不是重复元素(字符串中可能含有重复元素),避免【同层】重复搜索
            if (set.contains(c[i]))
                continue;
            set.add(c[i]);
            //将x与i位交换
            swap(x,i);
            dfs(x+1);
            //注意要换回来,因为这是同一轮(同一个位置)下,为了下一次固定数选其他位置交换要先还原
            swap(x,i);
        }
    }

    void swap(int a,int b)
    {
        char temp = c[a];
        c[a] = c[b];
        c[b] = temp;
    }

    public static void main(String[] args)
    {
        Solution sol = new Solution();
        String[] solution = sol.permutation(args[0]);
        Arrays.sort(solution);
        System.out.print("["+solution[0]);
        for (int i=1;i<solution.length;i++)
            System.out.print(","+solution[i]);
        System.out.print("]");
    }
}

路径搜索问题:寻找某个路径

核心问题:

遍历符合要求的路径才计数,路径走不通就返回

即整条递归路径(栈)符合要求才计数

思路:通过回溯返回值来标记符合要求的路径,当只要递归路径中所有结点全满足要求才返回满足要求

即通过返回值来标记该节点后面的所有节点组成的问题(递归子问题)是否满足要求


问题:请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一格开始,每一步可以在矩阵中向左、右、上、下移动一格。如果一条路径经过了矩阵的某一格,那么该路径不能再次进入该格子。例如,在下面的3×4的矩阵中包含一条字符串“bfce”的路径(路径中的字母用加粗标出)。

[[“a”,”b“,”c”,”e”],
[“s”,”f“,”c“,”s”],
[“a”,”d”,”e“,”e”]]

但矩阵中不包含字符串“abfb”的路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入这个格子。

示例 1:

输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

示例 2:

输入:board = [["a","b"],["c","d"]], word = "abcd"
输出:false

提示:

1 <= board.length <= 200
1 <= board[i].length <= 200


解答:

递归参数:坐标位置x,y,当前目标字符在字符串word中的索引

终止条件:①行列位置越界 或②当前矩阵元素与目标字符不同 或③当前矩阵已经访问过

P.S.注意由于可以从任何一个位置开始dfs,那么就有多个dfs过程,所以不能直接使用全局visited矩阵,但是如果使用局部visited矩阵,那每一次递归栈里都有一个visited矩阵,储存开销/时间开销巨大!

所以如果我们要使用全局的形式就需要每一次return就还原visited当前值矩阵!(看代码中“注意visited矩阵的处理”那段,进栈标记,以防子问题走到已经访问过的格子,出栈复原)

public class Offer12
{
    static boolean[][] visited;
    public static boolean exist(char[][] board,String word)
    {
        visited = new boolean[board.length][board.length];
        //每一个格子都可以是起点,对于每一个格子起点的dfs都有一个visited矩阵
        for(int i=0;i<board.length;i++)
            for(int j=0;j<board.length;j++)
                //以i,j为起点开始dfs
                if(dfs(board,word,i,j,0))
                    return true;
        return false;
    }

    public static boolean dfs(char[][] board,String word,int x,int y,int nowPointer)
    {
        //退出条件:①单元格撞墙或②单元格的内容与字符串索引位不匹配或③访问过
        if(x<0 || y<0 || x>=board.length || y>=board.length || board[x][y]!=word.charAt(nowPointer) || visited[x][y]==true)
            return false;

        //退出条件:遍历到字符串最后一个
        if(nowPointer==word.length()-1)
            return true;

        //回溯返回值:当前结点所有子问题如果有一条路可行(返回true)那就是true
        //递归入口:即探访子问题——可以向上下左右四个方向走
        //注意visited矩阵的处理,进栈流程中要标记为true,出栈流程中要复原回去
        visited[x][y] = true;
        boolean res = dfs(board,word,x+1,y,nowPointer+1) ||
                dfs(board,word,x-1,y,nowPointer+1) ||
                dfs(board,word,x,y+1,nowPointer+1) ||
                dfs(board,word,x,y-1,nowPointer+1);
        //出栈复原
        visited[x][y] = false;
        return res;
    }


    public static void main(String[] args)
    {
        char[][] board = new char[][]{{'A','B','C','E'},{'S','F','C','S'},{'A','D','E','E'}};
        String word = "ABCCED";
        
        System.out.println(exist(board,word));
    }
}

---
true    

路径记录问题:迷宫问题

在DFS的同时还需要记录DFS的搜索路径:使用一个list或stack保存路径!

具体地,出栈的时候,如果满足要求,就将该节点保存下来(入list或stack)

如果要保存多个路径:每次传递的时候就逐层累计记录下来已经走下的路径,走到终点判断该路可以就记录该累计变量or打印

变种:打印符合某条件的路径(一样的思路)

回溯返回值问题:树的深度

思路:

回溯返回值除了可以用于寻找满足条件的递归路径,还可以做很多事情,只要抓住一点,回溯返回值可以传递递归不同层级当前结点看得到的各种信息

树的深度其实就是求最长递归路径


解答:

注意观察路径记录问题和回溯返回值问题

路径记录问题,越往下递归路径越多,所以我们得前序遍历,递归到一个节点就执行执行体(记录该路径点)

回溯返回值问题,我们是要比较左右子树符合条件的路径,最终我们只要一条路径,所以得后序遍历,递归返回的时候才执行执行体(比较左右子树最深那个,返回深度值)

注意区别递归顺序和执行顺序


且听风吟