【图论搜索专题】如何使用「多源 BFS」降低时间复杂度

简介: 【图论搜索专题】如何使用「多源 BFS」降低时间复杂度

网络异常,图片无法展示
|


题目描述



这是 LeetCode 上的 [1162. 地图分析] ,难度为 中等


你现在手里有一份大小为 N * NNN 的 网格 gridgrid,上面的每个 单元格 都用 0011 标记好了。


其中 00 代表海洋,11 代表陆地,请你找出一个海洋单元格,这个海洋单元格到离它最近的陆地单元格的距离是最大的。


我们这里说的距离是「曼哈顿距离」:(x0, y0)(x0,y0)(x1, y1)(x1,y1) 这两个单元格之间的距离是 |x0 - x1| + |y0 - y1|x0x1+y0y1


如果网格上只有陆地或者海洋,请返回 -11


示例 1:


网络异常,图片无法展示
|


输入:[[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释:海洋单元格 (1, 1) 和所有陆地单元格之间的距离都达到最大,最大距离为 2。
复制代码


示例 2:


网络异常,图片无法展示
|


输入:[[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释:海洋单元格 (2, 2) 和所有陆地单元格之间的距离都达到最大,最大距离为 4。
复制代码


提示:


  • 1 <= grid.length == grid[0].length <= 100
  • grid[i][j] 不是 00 就是 11


单源 BFS



通常我们使用 BFS 求最短路,都是针对如下场景:从特定的起点出发,求解到达特定终点的最短距离。


这是一类特殊的「单源最短路」问题:本质是在一个边权为 11 的图上,求从特定「源点」出发到达特定「汇点」的最短路径。


对于本题,如果套用「单源最短路」做法,我们需要对每个「海洋」位置做一次 BFS:求得每个「海洋」的最近陆地距离,然后在所有的距离中取 maxmax 作为答案。


单次 BFS 的最坏情况需要扫描完整个矩阵,复杂度为 O(n^2)O(n2)


同时,最多有 n^2n2 个海洋区域需要做 BFS,因此这样的做法复杂度为 O(n^4)O(n4),并且 O(n^4)O(n4) 可直接取满。


PS. 数据范围为 10^2102,理论上是一定会超时,但本题数据较弱,Java 2021/06/28 可过。


一些细节:为了方便,我们在使用哈希表记录距离时,将二维坐标 (x, y)(x,y) 转化为对应的一维下标 idx = x * n + yidx=xn+y 作为 key 进行存储。


代码:


class Solution {
    int n;
    int[][] grid;
    public int maxDistance(int[][] _grid) {
        grid = _grid;
        n = grid.length;
        int ans = -1;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    ans = Math.max(ans, bfs(i, j));
                }
            }
        }
        return ans;
    }
    // 单次 BFS:求解海洋位置 (x,y) 最近的陆地距离
    int bfs(int x, int y) {
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        Deque<int[]> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        d.addLast(new int[]{x, y});
        map.put(x * n + y, 0);
        while (!d.isEmpty()) {
            int[] poll = d.pollFirst();
            int dx = poll[0], dy = poll[1];
            int step = map.get(dx * n + dy);
            if (grid[dx][dy] == 1) return step;
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                int key = nx * n + ny;
                if (map.containsKey(key)) continue;
                d.addLast(new int[]{nx, ny});
                map.put(key, step + 1);
            }
        }
        return -1;
    } 
}
复制代码


  • 时间复杂度:O(n^4)O(n4)
  • 空间复杂度:O(n^2)O(n2)


多源 BFS



这其实还是道「多源 BFS」入门题。


与「单源最短路」不同,「多源最短路」问题是求从「多个源点」到达「一个/多个汇点」的最短路径。


在实现上,最核心的搜索部分,「多源 BFS」与「单源 BFS」并无区别。


并且通过建立虚拟「超级源点」的方式,我们可以「多源 BFS」转换回「单源 BFS」问题。


什么意思?


以本题为例,题面要我们求每个「海洋」区域到最近的「陆地」区域的最大值。


我们可以将「源点/起点」和「汇点/终点」进行反转:从每个「陆地」区域出发,多个「陆地」区域每次同时向往扩散一圈,每个「海洋」区域被首次覆盖时所对应的圈数,就是「海洋」区域距离最近的「陆地」区域的距离。


网络异常,图片无法展示
|


不过,这是如何与「单源 BFS」联系起来的呢?


我们可以想象存在一个「虚拟源点」,其与所有「真实源点」(陆地)存在等权的边,那么任意「海洋」区域与「最近的陆地」区域的最短路等价于与「虚拟源点」的最短路:


网络异常,图片无法展示
|


实现上,我们并不需要真的将这个虚拟源点建立出来,只需要将所有的「真实源点」进行入队即可。


这个过程相当于从队列中弹出「虚拟源点」,并把它所能到点(真实源点)进行入队,然后再进行常规的 BFS 即可。


一些细节:实现上为了方便,在进行常规 BFS 时,如果一个「海洋」区域被访问到,说明其被离它「最近的陆地」覆盖到了,修改值为最小距离。这样我们只需要考虑那些值仍然为 00 的「海洋」区域即可(代表尚未被更新)。


代码:


class Solution {
    public int maxDistance(int[][] grid) {
        int n = grid.length;
        Deque<int[]> d = new ArrayDeque<>();
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    d.add(new int[]{i, j});
                    map.put(i * n + j, 0);
                }
            }
        }
        int ans = -1;
        int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}};
        while (!d.isEmpty()) {
            int[] poll = d.poll();
            int dx = poll[0], dy = poll[1];
            int step = map.get(dx * n + dy);
            for (int[] di : dirs) {
                int nx = dx + di[0], ny = dy + di[1];
                if (nx < 0 || nx >= n || ny < 0 || ny >= n) continue;
                if (grid[nx][ny] != 0) continue;
                grid[nx][ny] = step + 1;
                d.add(new int[]{nx, ny});
                map.put(nx * n + ny, step + 1);
                ans = Math.max(ans, step + 1);
            }
        }
        return ans;
    }
}
复制代码


  • 时间复杂度:O(n^2)O(n2)
  • 空间复杂度:O(n^2)O(n2)


总结



今天我们介绍了「多源 BFS」,通过建立「虚拟源点」,我们可以将其转化回「单源 BFS」问题。


实现上我们只需要将所有的「真实源点」进行入队,然后再进行 BFS 即可。


看起来两者区别不大,但其本质是通过源点/汇点转换,应用常规的 Flood Fill 将多次朴素 BFS 转化为一次 BFS,可以有效降低我们算法的时间复杂度。


最后



这是我们「刷穿 LeetCode」系列文章的第 No.1162 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先将所有不带锁的题目刷完。


在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。


为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour…


在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。

相关文章
|
数据采集 机器学习/深度学习 数据挖掘
python数据分析——数据预处理
数据预处理是数据分析过程中不可或缺的一环,它的目的是为了使原始数据更加规整、清晰,以便于后续的数据分析和建模工作。在Python数据分析中,数据预处理通常包括数据清洗、数据转换和数据特征工程等步骤。
768 0
|
Android开发 开发者
什么是Android Jetpack,它包括哪些组件?
什么是Android Jetpack,它包括哪些组件?
629 0
|
消息中间件 存储 缓存
QPS多少,才算高并发 ?
本文详解高并发概念及 QPS 标准,大厂面试高频点,建议掌握收藏。关注【mikechen的互联网架构】,10年+BAT架构经验分享。
QPS多少,才算高并发 ?
|
Python Windows
moviepy:基于 ffmpeg 的视频处理模块
moviepy:基于 ffmpeg 的视频处理模块
757 0
成功解决:snippet设置的开机自启没有效果
这篇文章分享了解决Snipaste软件设置开机自启无效的问题,通过将Snipaste的快捷方式放入Windows的开机启动目录来实现开机自动运行。
成功解决:snippet设置的开机自启没有效果
|
Ubuntu Linux 测试技术
探索Linux中的`dbus-send`命令
`dbus-send`是Linux中用于进程间通信的D-Bus系统的命令行工具,允许应用程序通过消息总线相互交互。要安装它,可以使用包管理器(如`apt-get`或`dnf`)。基本语法包括指定总线类型、目标服务、消息类型、对象路径、接口及方法等。示例用法包括使用`dbus-send`来锁定屏幕(通过调用`org.gnome.ScreenSaver.Lock`)和设置音量(通过与PulseAudio服务交互)。在使用时,需了解目标服务的接口和方法,并确保具备相应权限。
1075 10
|
存储 缓存 算法
C++链表解析:从基础原理到高级应用,全面掌握链表的使用
C++链表解析:从基础原理到高级应用,全面掌握链表的使用
1535 0
|
存储 监控 Ubuntu
如何在 Ubuntu 20.04 上安装 Grafana?
如何在 Ubuntu 20.04 上安装 Grafana?
1216 1
如何在 Ubuntu 20.04 上安装 Grafana?
|
消息中间件 存储 分布式计算
Flink实战(八) - Streaming Connectors 编程(上)
Flink实战(八) - Streaming Connectors 编程(上)
624 0
Flink实战(八) - Streaming Connectors 编程(上)