LeetCode刷题 -- BFS

“”前面接触了深度优先搜索(DFS),现在来介绍一下广度优先搜索(BFS)。

如果我们 只是为了遍历一棵树、一张图上的所有结点的话,那么 DFS BFS 的能力没什么差别,我们当然更倾向于更方便写、空间复杂度更低的 DFS 遍历。不过,某些使用场景是 DFS 做不到的,只能使用 BFS 遍历。这就是本文要介绍的两个场景:「层序遍历」、「最短路径」。

代码比较:

看以下两段代码,最直观的感受就是DFS比BFS的代码要简洁的多,这是因为递归调用的方式隐含的使用了系统的栈,我们不需要去自己在维护一个数据结构。如果简单的去遍历二叉树,DFS无疑是更加方便的一个选择。

DSF:

void dfs(TreeNode root) {
    if (root == null) {
        return;
    }
    dfs(root.left);
    dfs(root.right);
}

BFS

void bfs(TreeNode root) {
    Queue queue = new ArrayDeque<>();
    queue.add(root);
    while (!queue.isEmpty()) {
        // Java 的中队列采用offer/poll两个动作
        //堆栈采用push/pop,不要混合使用,也不要采用list的add方法,避免思维混乱
        TreeNode node = queue.poll(); 
        if (node.left != null) {
            queue.offer(node.left);
        }
        if (node.right != null) {
            queue.offer(node.right);
        }
    }
}

遍历顺序

虽然 DFS BFS 都是将二叉树的所有结点遍历了一遍,但它们遍历结点的顺序不同。 DFS是向下遍历,而BFS是按照层去遍历,这个遍历顺序也就是BFS能够用来解层序遍历」、「最短路径」两个问题的根本原因。在 BFS 遍历的基础上区分遍历的每一层,就得到了层序遍历在层序遍历的基础上记录层数,就得到了最短路径

LeetCode刷题 -- BFS_第1张图片

BFS解题模板

public int bfs() {
    // 定义一个队列
    Queue queue = new LinkedList<>();
    // 定义层数,默认为0
    int level = 0;
    // 将最初的beginWord加入到队列,后面开始BFS遍历
    queue.offer("beginWord");

    // 开始BFS遍历的循环
    // 循环条件是,定义的队列中有元素
    while (!queue.isEmpty()) {
        // 只要进入一次遍历,就代表层数+1;
        // 例如level初始化为0,遍历根元素的时候,就要从0加到1,
        level++;
        
        // 首先获得当前层,元素的个数
        // 因为后面遍历的过程中会不断的往queue中添加元素,如果不先记下这个个数,就可能会遍历到其他层去
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            // 当前层遍历到一个元素,就要把这个元素poll出来
            String poll = queue.poll();
            
            // 这下面的代码就要根据题目要求进行编写了
            if("等于目标值") {
                // 一般会要求返回最小路径长度之类的
                return level;
            }
            
            if("符合路径要求的元素") {
                // 将符合路径要求的元素加入队列中,后面进行下一层的遍历
                queue.offer("符合路径要求的元素");
                // 一般这个位置还会定义一个set,防止同一个元素被多次遍历
            }
        }
        
    }
}

例题

127. 单词接龙

题目

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列 beginWord -> s1 -> s2 -> ... -> sk

  • 每一对相邻的单词只差一个字母。
  •  对于 1 <= i <= k 时,每个 si 都在 wordList 中。注意, beginWord 不需要在 wordList 中。
  • sk == endWord

给你两个单词 beginWord 和 endWord 和一个字典 wordList ,返回 从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0 。

分析

最短转换序列 ”  考虑用 BFS求解

代码

public int ladderLength(String beginWord, String endWord, List wordList) {
    Set set = new HashSet<>(wordList);
    // 根据题意,如果给的wordlist为空或者不包括endWord,则没有转换路径,返回0;
    if (set.isEmpty() || !set.contains(endWord)) {
        return 0;
    }

    // 下面就是BFS的模板代码
    // 定义一个队列
    Queue queue = new LinkedList<>();
    // 定义转换次数,默认为0
    int level = 0;
    // 将最初的beginWord加入到队列,后面开始BFS遍历
    queue.offer(beginWord);

    // 只要队列不为空,就遍历
    while (!queue.isEmpty()) {
        // 只要进入一次遍历,就代表层数+1;
        level++;
        
        //遍历当前队列中的所有元素,先取得当前size
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            // 遍历的时候记得把当前这个字母从队列拿出来
            String currWord = queue.poll();
            // 如果遍历到了目标值,就直接返回层数
            if (currWord.equals(endWord)) {
                return level;
            }

            // 这个方法就是BFS遍历时做的判断
            for (int k = 0; k < currWord.length(); k++) {
                // 每一位都从a 遍历到 z
                for (char j = 'a'; j <= 'z'; j++) {
                    // 组装当前遍历到的单词
                    String newWord = currWord.substring(0, k) + j + currWord.substring(k + 1);

                    // 如果set中存在当前单词,代表这个单词是符合要求的,这个时候就可以进行下一层的遍历了
                    if (set.contains(newWord)) {
                        // 想要遍历下一层,首先要把当前单词加到队列中
                        queue.offer(newWord);
                        // 为了避免重复遍历,把已经遍历到的单词扔出去。
                        set.remove(newWord);
                    }
                }
            }
        }
    }
    return 0;
}

752. 打开转盘锁

题目

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

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

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

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

分析

“最小旋转次数” ---“最短路径”,直接考虑BFS求解

代码

public int openLock(String[] deadends, String target) {
    // 如果目标值直接等于0000,则不需要拨动就可以打开,返回0
    if ("0000".equals(target)) {
        return 0;
    }

    // 将不能拨的数放到一个set中
    Set dead = new HashSet();
    Collections.addAll(dead, deadends);
    // 如果目标值就在不能拨的值中,则返回-1
    if (dead.contains("0000")) {
        return -1;
    }
    
    // 下面开始BFS的代码模板
    int step = 0;
    // 定义一个队列,存放当前层要遍历的元素
    Queue queue = new LinkedList<>();
    // 把根元素加到队列中,即最开始的0000
    queue.offer("0000");
    // 定义一个set ,存放已经遍历过的元素,防止重复遍历
    Set seen = new HashSet<>();
    seen.add("0000");

    // 开始BFS遍历
    while (!queue.isEmpty()) {
        // 只要进入循环,层数就+1
        step++;
        // 遍历当前层的所有元素
        // queue先进先出,这个位置先定义当前queue的数量,
        // 即便后面queue在添加进新的元素,也没有关系,当前层结束后,就会结束这个循环
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            // 遍历到一个元素,就把他取出来
            String status = queue.poll();
            // 循环遍历当前元素通过一次拨动所有的结果
            for (String nextStatus : get(status)) {
                // 如果拨动一次得到的结果没有遍历过,并且不在死亡数组中,就可以进行下一步判断
                if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
                    // 如果当前结果正好是目标值,则可以直接返回step
                    if (nextStatus.equals(target)) {
                        return step;
                    }
                    // 否则,将当前值加入到queue中
                    queue.offer(nextStatus);
                    // 此外,还要将当前值加入到seen中,避免后续重复遍历。
                    seen.add(nextStatus);
                }
            }
        }
    }
    return -1;
}

// 当前数字,往小了拨动
public char numPrev(char x) {
    return x == '0' ? '9' : (char) (x - 1);
}

// 当前数字往大了拨动
public char numSucc(char x) {
    return x == '9' ? '0' : (char) (x + 1);
}

// 枚举 status 通过一次旋转得到的数字
// 如0000旋转一次可以得到:9000, 1000, 0900, 0100, 0090, 0010, 0009, 0001
public List get(String status) {
    List ret = new ArrayList();
    char[] array = status.toCharArray();
    // 因为是4位数字,因此要循环遍历四次
    for (int i = 0; i < 4; ++i) {
        // num存储当前位置的原始数字
        char num = array[i];
        // 往小了拨动得到的数放到list中
        array[i] = numPrev(num);
        ret.add(new String(array));
        // 往大了拨动得到的数放到list中
        array[i] = numSucc(num);
        ret.add(new String(array));
        // 记得将当前位置的数字复原
        array[i] = num;
    }
    return ret;
}

529. 扫雷游戏

题目:

LeetCode刷题 -- BFS_第2张图片

 分析:

可以尝试BFS进行解题

代码:

public class Test529_2 {
    // 因为每次遍历都要朝8个方向进行遍历,因此事先定义两个数组,
    // 这样就可以通过循环的方式去计算八个方向的数组位置
    int[] dx = {-1, 1, 0, 0, -1, 1, -1, 1};
    int[] dy = {0, 0, -1, 1, -1, 1, 1, -1};

    public char[][] updateBoard(char[][] board, int[] click) {
        int x = click[0];
        int y = click[1];
        // 1. 若起点是雷,游戏结束,直接修改 board 并返回。
        if (board[x][y] == 'M') {
            board[x][y] = 'X';
            return board;
        }
        int m = board.length;
        int n = board[0].length;
        
        // 定义一个二维数组,代表board对应位置已经访问过了。
        boolean[][] visited = new boolean[m][n];
        
        // 创建一个队列
        Queue queue = new LinkedList<>();
        // 把当前click添加到队列中
        queue.offer(click);
        // 当前点是开始点,肯定是访问过了,对应位置赋值为true
        visited[x][y] = true;

        // 开始循环遍历queue
        while (!queue.isEmpty()) {
            // 根据题意,不需要最短路径,因此不需要分层,上文也没有定义level
            // 取出最先插入的元素
            int[] poll = queue.poll();
            // 定义一个计数器,代表该点周围地雷的数量
            int count = 0;
            // 循环遍历八个方向的点,看是否存在地雷
            for (int k = 0; k < 8; k++) {
                int a = poll[0] + dx[k];
                int b = poll[1] + dy[k];
                // 超出数组范围就不用访问了
                if (a < 0 || a >= m || b < 0 || b >= n) {
                    continue;
                }
                // 如果遇到地雷,计数器就加1
                if (board[a][b] == 'M') {
                    count++;
                }
            }
            // 周围八个位置遍历完了,就可以对该点进行赋值操作了
            if (count > 0) {
                // 如果存在地雷,那么把地雷的数量赋值给该点
                board[poll[0]][poll[1]] = (char) (count + '0');
            } else {
                // 如果不存在地雷,那么就赋值为B,同时,还需要继续遍历周围的八个方向是否还存在空白方块
                board[poll[0]][poll[1]] = 'B';
                // 遍历八个方向
                for (int k = 0; k < 8; k++) {
                    int a = poll[0] + dx[k];
                    int b = poll[1] + dy[k];
                    // 此处做优化,超出数组范围的不访问
                    // 不为E的不访问(代表该处已经被挖过了)
                    // 已经访问过了不在访问
                    if (a < 0 || a >= m || b < 0 || b >= n || board[a][b] != 'E' || visited[a][b]) {
                        continue;
                    }
                    // 如果不满足上述的几个剔除条件,就要把该点放入队列中,进行下次遍历
                    // 同时,意味着该点已经被访问过了。visited标记为true
                    visited[a][b] = true;
                    queue.offer(new int[] {a, b});
                }
            }
        }
        return board;
    }
}

LeetCode刷题 -- BFS_第3张图片

尝试用DFS解决问题:

public class Test529 {
    // 因为每次遍历都要朝8个方向进行遍历,因此事先定义两个数组,
    // 这样就可以通过循环的方式去计算八个方向的数组位置
    int[] dx = {-1, 1, 0, 0, -1, 1, -1, 1};
    int[] dy = {0, 0, -1, 1, -1, 1, 1, -1};
    
    public char[][] updateBoard(char[][] board, int[] click) {
        int x = click[0];
        int y = click[1];
        // 1. 若起点是雷,游戏结束,直接修改 board 并返回。
        if (board[x][y] == 'M') {
            board[x][y] = 'X';
        } else { 
            // 2. 若起点是空地,则从起点开始向 8 邻域的空地进行深度优先搜索。
            dfs(board, x, y);
        }
        return board;
    }

    private void dfs(char[][] board, int i, int j) {
        // 定义计数器
        int cnt = 0;
        for (int k = 0; k < 8; k++) {
            int x = i + dx[k];
            int y = j + dy[k];
            // 超过数组范围的,跳过
            if (x < 0 || x >= board.length || y < 0 || y >= board[0].length) {
                continue;
            }
            // 如果存在地雷,那么计数器加1
            if (board[x][y] == 'M') {
                cnt++;
            }
        }
        // 如果该点存在地雷,那么就赋响应的数字,然后就可以退出递归了。
        if (cnt > 0) {
            board[i][j] =  (char)(cnt + '0');
            return;
        }

        // 若空地 (i, j) 周围没有雷,则将该位置修改为 ‘B’,向 8 邻域的空地继续搜索。
        board[i][j] = 'B';
        for (int k = 0; k < 8; k++) {
            int x = i + dx[k];
            int y = j + dy[k];
            // 超过数组范围的,跳过
            // 不为E的,代表已经访问过了,因此也跳过。
            if (x < 0 || x >= board.length || y < 0 || y >= board[0].length || board[x][y] != 'E') {
                continue;
            }
            // 递归调用
            dfs(board, x, y);
        }
    }
}

LeetCode刷题 -- BFS_第4张图片

​​​​​​994. 腐烂的橘子

题目:

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

值 0 代表空单元格;
值 1 代表新鲜橘子;
值 2 代表腐烂的橘子。
每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1 

分析:

最小分钟数,可以用BFS搜索

代码:

public int orangesRotting(int[][] grid) {
    int m = grid.length;
    int n = grid[0].length;
    // 定义四个方向
    int[] dx = {1, -1, 0, 0};
    int[] dy = {0, 0, 1, -1};
    
    // 定义队列和visited
    Queue queue = new LinkedList<>();
    boolean[][] visited = new boolean[m][n];

    // 注意这个位置,如果原始数组中就没有等于1的,那么要直接返回0;
    boolean hasFresh = false;
    // 遍历数组,将等于2的都放到队列中,并标记为已经访问
    for (int i = 0; i < m; i++) {
        for (int j = 0; j < n; j++) {
            if (grid[i][j] == 2) {
                queue.offer(new int[] {i, j});
                visited[i][j] = true;
            } 
            if (grid[i][j] == 1) {
                hasFresh = true;
            }
        }
    }
    if (!hasFresh) {
        return 0;
    }
    
    // 开始进行BFS
    int level = 0;
    while (!queue.isEmpty()) {
        level++;
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            int[] poll = queue.poll();
            // 对队列中的每个点进行上下左右四个方向的遍历
            for (int j = 0; j < dx.length; j++) {
                int x = poll[0] + dx[j];
                int y = poll[1] + dy[j];
                // 超出数组范围就跳过
                if (x < 0 || y < 0 || x >= m || y >= n) {
                    continue;
                }
                // 如果没有被访问过,并且值为1,就说明这个被污染了
                if (!visited[x][y] && grid[x][y] == 1) {
                    // 将这个位置加入队列,并标记为2
                    visited[x][y] = true;
                    grid[x][y] = 2;
                    queue.offer(new int[] {x,y});
                }
            }
        }
    }

    // 最后在遍历数组,如果存在等于1 的,说明有没被污染的,返回-1
    for (int[] ints : grid) {
        for (int j = 0; j < n; j++) {
            if (ints[j] == 1) {
                return -1;
            }
        }
    }
    // 否则返回level-1;
    return level - 1;
}

934. 最短的桥

题目

给你一个大小为 n x n 的二元矩阵 grid ,其中 1 表示陆地,0 表示水域。岛 是由四面相连的 1 形成的一个最大组,即不会与非组内的任何其他 1 相连。grid 中 恰好存在两座岛 。你可以将任意数量的 0 变为 1 ,以使两座岛连接起来,变成 一座岛 。返回必须翻转的 0 的最小数目。

分析

反转的0的最小数目,可以考虑用BFS来做。

题目中说岛恰好是两个,可以考虑如下思路:将其中一座岛上的数组统一标记为-1,将这个岛进行外扩,每次扩的距离是1,如果扩n次后,两个岛屿相连在一起,说明已经连上了,这个n就是我们要求的值。

代码

public class Test934 {
    // 定义四个方向
    int[] dx = {1, -1, 0, 0};
    int[] dy = {0, 0, 1, -1};
    // 定义一个set,防止queue里面插入重复的数据
    Set arraySet = new HashSet<>();
    
    public int shortestBridge(int[][] grid) {
        int length = grid.length;
        // 定义一个队列
        Queue queue = new LinkedList<>();
        
        // 遍历二维数组,遇到一个为1的点后,就dfs遍历,将值改成-1;
        // 因为题目中有两个岛,我们只需要将一个岛的值改成-1就可以了,
        // 因此定义一个bool值,只要遍历一个岛后,就赋值为true,防止后续继续遍历
        boolean a = false;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid.length; j++) {
                if (!a && grid[i][j] == 1) {
                    dsf(grid, i, j,queue);
                    break;
                }
            }
        }

        // 定义一个visited,开始BFS遍历
        boolean[][] visited = new boolean[length][length];
        // 题目要求最短的桥,这个level应该从-1开始
        int level = -1;
        while (!queue.isEmpty()) {
            level++;
            int size = queue.size();
            for (int k = 0; k < size; k++) {
                int[] poll = queue.poll();
                if (isConnect(grid, poll[0], poll[1], visited, queue)) {
                    return level;
                } 
            }
        }
        return level;
    }

    // 判断两个桥是否连通了,判断条件就是该点的上下左右,既有-1,也有1(另外一座桥)
    private boolean isConnect(int[][] grid, int x, int y, boolean[][] visited, Queue queue) {
        Set set = new HashSet<>();
        for (int i = 0; i < dx.length; i++) {
            int m = x + dx[i];
            int n = y + dy[i];
            if (m < 0 || n < 0 || m >= grid.length || n >= grid.length) {
                continue;
            }
            set.add(grid[m][n]);
            // 如果当前值为0,并且没被访问过,并且queue中也不含有这个点,就加入队列
            if (grid[m][n] == 0 && !visited[m][n] && !arraySet.contains(Arrays.toString(new int[] {m, n}))) {
                // 加入队列同时,设置该点值为-1,并且标记为已经访问
                queue.offer(new int[] {m, n});
                arraySet.add(Arrays.toString(new int[] {m, n}));
                visited[m][n] = true;
                grid[m][n] = -1;
            }
        }
        return set.contains(-1) && set.contains(1);
    }

    // dfs调用,吧其中一个岛的值都改成-1,同时将这个到的点都放到queue中
    private void dsf(int[][] grid, int x, int y,Queue queue) {
        if (x < 0 || y < 0 || x >= grid.length || y >= grid.length) {
            return;
        }
        if (grid[x][y] != 1) {
            return;
        }
        grid[x][y] = -1;
        queue.offer(new int[] {x, y});
        arraySet.add(Arrays.toString(new int[] {x, y}));
        dsf(grid, x + 1, y,queue);
        dsf(grid, x - 1, y,queue);
        dsf(grid, x, y + 1,queue);
        dsf(grid, x, y - 1,queue);
    }
}

你可能感兴趣的:(算法刷题,leetcode,宽度优先,深度优先)