对于Tree的BFS(典型的「单源 BFS」)大家都比较熟悉:首先把root节点入队,再一层一层无脑遍历就行了。
对于图的BFS(「多源 BFS」)也是一样,与Tree的BFS的区别如下:
-
Tree只有1个root,而图可以有多个源点,所以首先需要
把多个源点都入队
。 -
Tree是有向的因此不需要标识是否访问过,而对于无向图来说,必须得
标志是否访问过
哦!并且为了防止某个节点多次入队,需要在其入队之前就将其设置成已访问
!
下面通过几道leetcode题目的练习来掌握图的多源BFS的使用。
话不多说,进入正题。
文章目录
1162. 地图分析
1. 题目描述
leetcode题目链接:1162. 地图分析
2. 思路分析
题目要求先找到离陆地最远的海洋,怎么找到最远的海洋呢?
- 先把所有的陆地都入队
- 然后从各个陆地同时开始一圈一圈的向海洋扩散,
- 那么最后扩散到的海洋就是最远的海洋,并且这个海洋肯定是被离他最近的陆地给扩散到的!
下面是扩散的图示,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个节点)则就是最终的根节点。
这里无向图的构建使用了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] 是由其上下左右四个状态来决定,无法从一个方向开始递推!
于是我们尝试将问题分解:
- 距离 (i, j) 最近的 0 的位置,是在其 「左上,右上,左下,右下」4个方向之一;
- 因此我们分别从四个角开始递推,就分别得到了位于「左上方、右上方、左下方、右下方」距离 (i, j) 的最近的 0 的距离,取 min即可;
- 通过上两步思路,我们可以很容易的写出 4 个双重 for 循环,动态规划的解法写到这一步其实已经完全 OK 了;
- 如果第三步你还不满足的话,从四个角开始的 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。
- 首先将全部水域入队,且将水域置为0,
- 然后从各个 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总结
需要注意的是:
- 首先入队,是要把谁入队,是0还是1。要根据题目搞清楚。
- 1入队计算0,只需要入队即可。0入队计算1,需要将1初始化为-1,表示未被访问的1。
参考: