BFS就这么点东西,看完还不会么

871 阅读3分钟

在上一篇文章中回溯+剪枝精髓都给你总结在这了我们总结了回溯算解题套路,归纳了关键点,大多数情况下回溯算法本质上就是在进行一个DFS搜索,那么今天我们就一起来看下BFS,看看它又有哪些招!

回忆下DFS:面前有多条路可以选择,我们总是选择其中一条,然后一直向前走,发现不满足,然后返回回来,换另外一条,所以在DFS中我们在不断的尝试、返回。有可能存在这么一种情况,面前的道路中其中有一条我们只需要向前迈一步,就到达了,如果此时我们还是按照DFS的思想是不是要做很多无用功,所以这时候BFS就可以发挥它的优势了。

再举个形象的例子:方形的鱼塘里有条大鱼,捕鱼者站在窄边上跃跃欲试,怎样才能抓到这条鱼呢,要么派个人下去,从这头摸到那头,没有的话再回来换条道儿,比较典型的DFS搜索模型,很显然不太可取。最简单的是不是两个人拉个大网,从这头走到那头,这样鱼就能最快被抓到,这就是BFS搜索的模型,所以BFS就是为了解决这类最短路径问题

我们可以看到DFS更多的像是一个点,不断的向前去探索,碰壁了就回来;而BFS像是一个面,整体齐头并进,这样我们就能很容易的知道两点之间的最短路径!

再来看下BFS的基本框架:

        //队列记录节点
        Queue<Node> queue = new LinkedList<>();
        //将最初始的节点添加进去
        queue.offer(root);
        //记录步数
        int steps = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            //取出每一层的节点,并把他们的children添加进队列
            for (int i = 0; i < size; i++) {
                Node poll = queue.poll();
                for (int j = 0; j < poll.children.size(); j++) {
                    queue.offer(poll.children.get(j));
                }
            }  
            //更新步数
            steps++;
        }

简单归纳下,就是队列中弹出某一层元素,然后将该层元素的children添加到队列中,然后继续循环往复。有了这个框架,类似N叉树的层序遍历、N叉树的最大、最小高度就很好解决了。敲黑板!!最基本框架,不仅仅是理解,还要能直接背诵默写出来!

接下来还是借助实际例子来进一步体会BFS思想!

leetcode-cn.com/problems/pe…

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。给你一个整数 n ,返回和为 n 的完全平方数的 最少数量。

分析下,题目是要求我们平方数个数最少,那么我们尝试能不能转化成BFS思想。

我们把n当做起点,0当做终点,如何从起点走到终点呢?试想对于给定的n,可用的完全平方数是不是总是固定的,因为这些平方数必须小于n,如n为13,那么完全平方数就只能是1,4,9。这些平方数就是站在n点的选择,要达到0,那么我们就用n减去这些平方数,如果减后为0了,就达到了终点。这时候统计步数就行,如果还不是0呢?那么就继续拿减后的值当做新的n,重新列举新的平方数组合,继续上面的步骤,直至得到0。以n=13为例,来看下搜索示意图:

Lark20210329-175230.png

结合我们的框架和上面解析,代码就很好写了:

    public int numSquares(int n) {
        int res = 0;
        Queue<Integer> queue = new LinkedList<>();
        //记录访问过的节点,去重
        HashSet<Integer> set = new HashSet<>();
        queue.add(n);
        set.add(n);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                int poll = queue.poll();
                //到达终点
                if (poll == 0) {
                    return res;
                }
                //列举满足条件的平方数
                for (int j = 1; j * j <= poll; j++) {
                    int remind = poll - j * j;
                    if (set.contains(remind)) {
                        continue;
                    }
                    queue.offer(remind);
                    set.add(remind);
                }
            }
            res++;
        }
        return res;
    }

这里唯一需要注意一点的是我们需要对节点进行去重,可以避免大量重复计算。举个例子,我们在第2层碰到了一个数a,等到遍历到第5层的时候,又碰到了a,如果经过a的路径最终可以找到0,那么肯定经过第2层的路径最短。即使不能找到,那么碰到第5层的a也没必要继续了。

继续来看leetcode-cn.com/problems/op…

Lark20210330-152147.png

要求我们求最小旋转次数,同样我们看是否满足BFS模型,我们把"0000"做为起点,终点为target,现在继续看站在每一个点面临的选择是否确定,很显然,不然我们走到哪一步,我们总是面临8个选择,4个索引位置上+1,或者-1,这里不一样的是埋了些雷,有些位置不能踩,就是我们的deadends数组,碰到我们直接过滤掉就好了,当我们碰到tartget时,统计走的步数是不是就可以了。套框架上代码:

    public int openLock(String[] deadends, String target) {
        //用来保存死亡数字和已访问过的点
        HashSet visited = new HashSet();
        Queue<String> queue = new LinkedList<>();
        for (String deadend : deadends) {
            if (deadend.equals("0000")) {
                return -1;
            }
            visited.add(deadend);
        }
        queue.offer("0000");
        visited.add("0000");
        int step = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String poll = queue.poll();
                //找到target返回步数
                if (poll.equals(target)) {
                    return step;
                }
                //每一位上都可以+1或者-1,那么罗列出所有情况
                for (int j = 0; j < 4; j++) {
                    String add = plusOne(poll, j);
                    if (!visited.contains(add)) {
                        queue.offer(add);
                        visited.add(add);
                    }
                    String minus = minusOne(poll, j);
                    if (!visited.contains(minus)) {
                        queue.offer(minus);
                        visited.add(minus);
                    }
                }
            }
            step++;
        }
        return -1;
    }
    
        // 将 s[i] 向上拨动一次
    private 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] 向下拨动一次
    private 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);
    }

这里同样需要注意下,回拨点和雷点是都需要过滤,所以我们用一个HashSet来记录,碰到后直接跳过。

总结一下,相比起DFS,本质上他们都是为了进行遍历搜索。BFS需要分配额外的内存空间来进行辅助,所占用的内存更大,而DFS往往需要进行递归,当递归深度过大时也容易造成堆栈溢出。在处理最短路径上时,BFS往往更有优势。总的来说BFS和DFS各有优势利弊,有些时候两者皆可使用差别不大,而有的时候需要根据实际情况来选择适合的,需要我们对这两种方式深入理解体会!