【Leetcode】图的多源BFS详解

对于Tree的BFS典型的「单源 BFS」)大家都比较熟悉:首先把root节点入队,再一层一层无脑遍历就行了。

对于图的BFS「多源 BFS」)也是一样,与Tree的BFS的区别如下:

  1. Tree只有1个root,而图可以有多个源点,所以首先需要把多个源点都入队

  2. Tree是有向的因此不需要标识是否访问过,而对于无向图来说,必须得标志是否访问过哦!并且为了防止某个节点多次入队,需要在其入队之前就将其设置成已访问

下面通过几道leetcode题目的练习来掌握图的多源BFS的使用。

话不多说,进入正题。

1162. 地图分析

1. 题目描述

leetcode题目链接:1162. 地图分析
在这里插入图片描述

2. 思路分析

题目要求先找到离陆地最远的海洋,怎么找到最远的海洋呢?

  1. 先把所有的陆地都入队
  2. 然后从各个陆地同时开始一圈一圈的向海洋扩散,
  3. 那么最后扩散到的海洋就是最远的海洋,并且这个海洋肯定是被离他最近的陆地给扩散到的!

下面是扩散的图示,1表示陆地,0表示海洋。每次扩散的时候会标记相邻的4个位置的海洋:
在这里插入图片描述

3. 参考代码
class Solution {
    public int maxDistance(int[][] grid) {
        int[] dx = new int[]{0, 0, 1, -1};
        int[] dy = new int[]{1, -1, 0, 0};
        Queue<int[]> queue = new ArrayDeque<>();
        int n = grid.length;
        // 将所有陆地入队
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    queue.offer(new int[]{i, j});
                }
            }
        }
        // 全是海洋和陆地,返回-1
        if (queue.size() == 0 || queue.size() == n * n) {
            return -1;
        }
        // 从各个陆地开始,一圈一圈遍历海洋
        int[] node = null;
        while(!queue.isEmpty()) {
            node = queue.poll();
            int x = node[0], y = node[1];
            for (int i = 0; i < 4; i++) {
                int newX = x + dx[i];
                int newY = y + dy[i];
                // 越界或不是海洋
                if (newX < 0 || newX >= n || newY < 0 || newY >= n || grid[newX][newY] != 0) {
                    continue;
                }
                // 直接修改原数组,不需要额外数组标记访问
                grid[newX][newY] = grid[x][y] + 1;
                queue.offer(new int[]{newX, newY});
            }
        }
        // 返回最后一次遍历到的海洋的距离-1
        return grid[node[0]][node[1]] - 1;
    }
}

这里对全是海洋陆地的情况也可以设置一个布尔变量,遍历过后再判断:

boolean hasOcean = false;
……
// 没有陆地或者没有海洋,返回-1。
if (node == null || !hasOcean) {
    return -1;
}

310. 最小高度树

1. 题目描述

leetcode题目链接:310. 最小高度树
在这里插入图片描述在这里插入图片描述

2. 思路分析

本题的其中一种解法就是图的BFS:但多源BFS不是最佳的解题思路,这里是使用多源BFS的方法进行练习。

  1. 首先把各个叶子节点(入度为1的节点)全部入队,
  2. 一层一层的剥掉最外层的叶子结点,那么最后剩下的1个节点(或2个节点)则就是最终的根节点。

这里无向图的构建使用了List来实现,便于剥除叶子节点。

List<List<Integer>> degree = new ArrayList<>();
for (int i = 0; i < n; i++) {
    degree.add(new ArrayList<Integer>());
}
// 初始化构建图
for (int[] edge : edges) {
    degree.get(edge[0]).add(edge[1]);
    degree.get(edge[1]).add(edge[0]);
}

最后判断剩下一个节点还是两个节点。

if (remain == 1) {
    res.add(tmp);
} else {
    for(int i = 0; i < n; i++) {
        if (degree.get(i).size() == 1) {
            res.add(i);
        }
    }
}
3. 参考代码
class Solution {
    public List<Integer> findMinHeightTrees(int n, int[][] edges) {
        List<List<Integer>> degree = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            degree.add(new ArrayList<Integer>());
        }
        // 初始化构建图
        for (int[] edge : edges) {
            degree.get(edge[0]).add(edge[1]);
            degree.get(edge[1]).add(edge[0]);
        }
        int remain = n;
        int tmp = 0;
        Queue<Integer> queue = new LinkedList<>();
        while (remain > 2) {  // 最后剩下的1个节点(或2个节点)则就是最终的根节点。
            for (int i = 0; i < n; i++) {
                if(degree.get(i).size() == 1) {  // 所有叶子节点入队
                    queue.add(i);
                }
            }
            while (!queue.isEmpty()) {  // 剥掉最外层叶子节点
                int node = queue.poll();
                remain--;
                tmp = degree.get(node).get(0);
                degree.get(node).remove(0);  // 去掉叶子节点的度
                degree.get(tmp).remove(new Integer(node)); // 去掉非叶子节点对应叶子节点的度
            }
        }
        List<Integer> res = new ArrayList<>();
        if (remain == 1) {
            res.add(tmp);
        } else {
            for(int i = 0; i < n; i++) {
                if (degree.get(i).size() == 1) {
                    res.add(i);
                }
            }
        }
        return res;
    }
}

542. 01 矩阵

1. 题目描述

leetcode题目链接:542. 01 矩阵
在这里插入图片描述

2. 思路分析

方法一:多源BFS

这道题和1162. 地图分析一样,首先把每个源点 0 入队,然后从各个 0 同时开始一圈一圈的向 1 扩散(每个 1 都是被离它最近的 0 扩散到的 ),这里要注意先把 mat 数组中 1 的位置设置成 -1

方法二:动态规划

对于任一点 (i, j) ,距离 0 的距离为:
在这里插入图片描述因此我们用 dp[i][j] 表示该位置距离最近的 0 的距离

我们发现 dp[i][j] 是由其上下左右四个状态来决定,无法从一个方向开始递推!

于是我们尝试将问题分解:

  1. 距离 (i, j) 最近的 0 的位置,是在其 「左上,右上,左下,右下」4个方向之一;
  2. 因此我们分别从四个角开始递推,就分别得到了位于「左上方、右上方、左下方、右下方」距离 (i, j) 的最近的 0 的距离,取 min即可;
  3. 通过上两步思路,我们可以很容易的写出 4 个双重 for 循环,动态规划的解法写到这一步其实已经完全 OK 了;
  4. 如果第三步你还不满足的话,从四个角开始的 4 次递推,其实还可以优化成从任一组对角开始的 2 次递推,比如只写从左上角、右下角开始递推就行了。
3. 参考代码

方法一:多源BFS

class Solution {
    public int[][] updateMatrix(int[][] mat) {
        int[] dx = new int[]{0, 0, 1, -1};
        int[] dy = new int[]{1, -1, 0, 0};
        Queue<int[]> queue = new LinkedList<>();
        int m = mat.length, n = mat[0].length;
        // 首先将所有的 0 都入队,并且将 1 的位置设置成 -1,表示该位置是 未被访问过的 1
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (mat[i][j] == 0) {
                    queue.add(new int[]{i, j}); // 0入队
                } else {
                    mat[i][j] = -1;  // 表示未被访问的1
                }
            }
        }
        while(!queue.isEmpty()) {
            int[] node = queue.poll();
            int x = node[0], y = node[1];
            for (int i = 0; i < 4; i++) {
                int newX = x + dx[i];
                int newY = y + dy[i];
                if (newX < 0 || newX >= m || newY < 0 || newY >= n || mat[newX][newY] != -1) {
                    continue;
                }
                mat[newX][newY] = mat[x][y] + 1;
                queue.add(new int[]{newX, newY});
            }
        }
        return mat;
    }
}

方法二:动态规划

class Solution {
    public int[][] updateMatrix(int[][] mat) {
        int m = mat.length, n = mat[0].length;
        int[][] dp = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                dp[i][j] = mat[i][j] == 0 ? 0 : 10000;
            }
        }
        // 从左上角开始
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (i - 1 >= 0) {
                    dp[i][j] = Math.min(dp[i][j], dp[i - 1][j] + 1);
                }
                if (j - 1 >= 0) {
                    dp[i][j] = Math.min(dp[i][j], dp[i][j - 1] + 1);
                }
            }
        }
        // 从右下角开始
        for (int i = m - 1; i >= 0; i--) {
            for (int j = n - 1; j >= 0; j--) {
                if (i + 1 < m) {
                    dp[i][j] = Math.min(dp[i][j], dp[i + 1][j] + 1);
                }
                if (j + 1 < n) {
                    dp[i][j] = Math.min(dp[i][j], dp[i][j + 1] + 1);
                }
            }
        }
        return dp;
    }
}

1765. 地图中的最高点

1. 题目描述

leetcode题目链接:1765. 地图中的最高点
在这里插入图片描述在这里插入图片描述在这里插入图片描述

2. 思路分析

这道题与1162. 地图分析同样一样,不过需要注意的是题目中0表示陆地,1表示水域,且水域的高度必须是0。

  1. 首先将全部水域入队,且将水域置为0,
  2. 然后从各个 1 同时开始一圈一圈的向 0 扩散(每个 0 都是被离它最近的 1 扩散到的 ),同样这里需要将0初始置为-1,表示没有被访问。
3. 参考代码
class Solution {
    public int[][] highestPeak(int[][] isWater) {
        int[] dx = new int[]{0, 0, 1, -1};
        int[] dy = new int[]{1, -1, 0, 0};
        Queue<int[]> queue = new LinkedList<>();
        int m = isWater.length, n = isWater[0].length;
        // 所有的水域入队
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (isWater[i][j] == 1) {
                    queue.add(new int[]{i, j});  // 水域入队
                    isWater[i][j] = 0; // 且高度置为0
                } else {
                    isWater[i][j] = -1;  // 陆地初始置为-1,表示未被访问
                }
            }
        }
        while(!queue.isEmpty()) {
            int[] node = queue.poll();
            int x = node[0], y = node[1];
            for (int i = 0; i < 4; i++) {
                int newX = x + dx[i];
                int newY = y + dy[i];
                if (newX < 0 || newX >= m || newY < 0 || newY >= n || isWater[newX][newY] != -1) {
                    continue;
                }
                isWater[newX][newY] = isWater[x][y] + 1;
                queue.add(new int[]{newX, newY});
            }          
        }
        return isWater;
     }
}

图的多源BFS总结

需要注意的是:

  1. 首先入队,是要把谁入队,是0还是1。要根据题目搞清楚。
  2. 1入队计算0,只需要入队即可。0入队计算1,需要将1初始化为-1,表示未被访问的1。

参考:

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值