BFS算法解题套路

76 阅读6分钟

1 BFS算法解题套路框架

BFS算法的核心思想为:

将一些问题抽象为图,从一个点开始,向四周扩散。一般来说啊,BFS算法都是用到[队列]的,将一个节点的周围的所有节点加入到队列。

BFS算法和DFS算法主要的区别为:

  • BFS算法找到的路径一定是最短的,但是代价就是空间复杂度比DFS大很多
  • DFS算法适合寻找通路,即是否存在路径的问题,当然也用于解决连通性,拓扑排序等问题

BFS算法问题的本质就是在一幅【图】中找到从起始点start到终点end的最近距离。

常见的题目类型有:

  • 走迷宫
  • 两个单词,通过某些操作,将一个变为另一个
  • 连连看游戏

不管题目的形式怎么改变,万变不离其宗,BFS的解题框架需要记牢:

int BFS (Node start, Node target) {
    // 核心数据结构,队列
    Queue<Node> q;
    // 避免走回头路
    Set<Node> visited;
    
    // 将起点加入队列
    q.offer(start);
    visited.add(start);
    
    while(q not empty) {
        int size = q.size();
        // 将当前队列中的节点向四周扩散
        for (int i = 0; i < size; i++) {
            Node cur = q.poll();
            // 判断是否达到重点
            if (cur is target) {
                return step;
            }
            // 将相邻的节点加入队列
            for (Node neighbor : cur.neighbors) {
                if (neighbor have not visited) {
                    q.offer(beighbor);
                    visited.add(neighbor);
                }
            }
        }
    }
    // 图中没有目标节点
}

2 实战训练

本节通过两道BFS经典题目,[二叉树最小高度]和[打开转盘锁]两道题深入体会BFS算法的核心思路。

2.1 二叉树最小高度

题目描述

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

**说明:**叶子节点是指没有子节点的节点。

我的思路

  • 对二叉树进行层序遍历,层序遍历也就是BFS
  • 维护一个记录深度的变量minHeight,每遍历一层,这个值就加1
  • 如果到达叶子节点,就直接返回树的深度

实现代码见[T111-二叉树最小深度]

在实现代码中,while循环和for循环来共同控制一层一层往下遍历每个节点,其中while循环控制一层一层往下走,而for循环控制每一层从左到右每一层二叉树节点。

从这个简单的题目中,我们可以寻找到下面这个问题的答案了:

  • 为什么BFS可以找到最短距离?

    我们观察DFS的逻辑,depth每往前走一步,队列中所有的节点都往前迈一步,这样保证到达目标节点,即终点的时候,走的步数是最小的。

    DFS可以找到最短路径,但是需要对每个可以到达终点的路径进行穷举,然后对比才能找到,这样时间复杂度非常高。

2.2 解开密码锁的最少次数

题目描述

你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每个拨轮可以自由旋转:例如把 '9' 变为 '0''0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。

锁的初始数字为 '0000' ,一个代表四个拨轮的数字的字符串。

列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,无法再被旋转。

字符串 target 代表可以解锁的数字,你需要给出解锁需要的最小旋转次数,如果无论如何不能解锁,返回 -1

示例

输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202"
输出:6
解释:
可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。
注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的,
因为当拨动到 "0102" 时这个锁就会被锁定。

我的思路

  • 队列中存储的是什么,当前锁的状态,锁的状态用什么存储呢?char[4] curLock
  • while()控制所有的状态拨一次,for控制当前状态的相邻状态
  • 如果遇到死锁,则不保存状态Set<String> deadlock

实现代码见[T752-打开转盘锁]

3 双向BFS优化

BFS算法有一种稍微高级的优化思路:双向BFS,可以进一步提高算法的效率。

传统的BFS算法是从起点开始向四周扩散,遇到终点时停止,而双向BFS则是从起点和终点同时开始扩散,当两边有交集的时候停止

双向BFS亦有局限性,必须知道终点在哪里。

[打开转盘锁]双向BFS优化之后的代码为:

/**
 * BFS 双向扩散优化代码
 * @param deadends
 * @param target
 * @return
 */
public int openLockII(String[] deadends, String target) {
    // 记录死锁密码
    HashSet<String> deadlocks = new HashSet<>(Arrays.asList(deadends));

    // 记录已经穷举过的代码
    HashSet<String> tried = new HashSet<>();
    // 两个集合,分别表示 源扩散 元素和 目标扩散 元素
    HashSet<String> source = new HashSet<>();
    HashSet<String> destination = new HashSet<>();

    int step = 0;
    source.add("0000");
    destination.add(target);
    while (!source.isEmpty() && !destination.isEmpty()) {
        // 使用temp存储扩散结果
        HashSet<String> temp = new HashSet<>();

        // 将 source 节点向周围扩散
        for (String cur : source) {
            // 判断是否到达了终点
            if (deadlocks.contains(cur)) continue;
            if (destination.contains(cur)) return step;
            tried.add(cur);

            // 将当前节点的相邻节点加入到集合中
            for (int i = 0; i < cur.length(); i++) {
                StringBuilder up = new StringBuilder();
                StringBuilder down = new StringBuilder();
                for (int j = 0; j < cur.length(); j++) {
                    if (j == i) {
                        char newUp = cur.charAt(j) == '9' ? '0' : (char) (cur.charAt(j) + 1);
                        up.append(newUp);
                        char newDown = cur.charAt(j) == '0' ? '9' : (char) (cur.charAt(j) - 1);
                        down.append(newDown);
                    } else {
                        up.append(cur.charAt(j));
                        down.append(cur.charAt(j));
                    }
                }
                if (!tried.contains(up.toString())) {
                    temp.add(up.toString());
                }
                if (!tried.contains(down.toString())) {
                    temp.add(down.toString());
                }

            }
        }
        // 增加步数
        step++;
        // 交换source 和 destination
        source = destination;
        destination = temp;
    }

    return -1;
}

BFS经典算法题

T111-二叉树最小深度

实现代码

/**
     * 二叉树的最小深度
     * @param root
     * @return
     */
public int minDepth(TreeNode root) {

    if (root == null) return 0;
    Queue<TreeNode> q = new LinkedList<>();
    q.offer(root);
    int minDepth = 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 minDepth;
            }
            if (cur.left != null) {
                q.offer(cur.left);
            }
            if (cur.right != null) {
                q.offer(cur.right);
            }
        }
        minDepth++;
    }
    return minDepth;
}

T752-打开转盘锁

实现代码

public int openLockI(String[] deadends, String target) {
    HashSet<String> deadlocks = new HashSet<>(Arrays.asList(deadends));
    if (deadlocks.contains("0000") || deadlocks.contains(target)) return -1;

    // 记录试过的密码
    HashSet<String> tried = new HashSet<>();
    tried.add("0000");

    // 记录当前状态列表
    LinkedList<String> q = new LinkedList<>();
    q.offer("0000");

    int step = 0;

    while (!q.isEmpty()) {
        int sz = q.size();
        for (int i = 0; i < sz; i++) {
            String curLock = q.poll();
            if (curLock.equals(target)) {
                return step;
            }
            for (int j = 0; j < curLock.length(); j++) {
                StringBuilder up = new StringBuilder();
                StringBuilder down = new StringBuilder();
                for (int k = 0; k < curLock.length(); k++) {
                    if (k == j) {
                        char newUp = curLock.charAt(k) == '9' ? '0' : (char) (curLock.charAt(k) + 1);
                        up.append(newUp);
                        char newDown = curLock.charAt(k) == '0' ? '9' : (char) (curLock.charAt(k) - 1);
                        down.append(newDown);
                    } else {
                        up.append(curLock.charAt(k));
                        down.append(curLock.charAt(k));
                    }
                }
                if (!deadlocks.contains(up.toString()) && !tried.contains(up.toString())) {
                    q.offer(up.toString());
                    tried.add(up.toString());
                }

                if (!deadlocks.contains(down.toString()) && !tried.contains(down.toString())) {
                    q.offer(down.toString());
                    tried.add(down.toString());
                }

            }
        }
        step++;
    }
    return -1;
}