题目描述
这是 LeetCode 上的 1020. 飞地的数量 ,难度为 中等。
Tag : 「DFS」、「并查集」
给你一个大小为 m x nmxn 的二进制矩阵 gridgrid ,其中 00 表示一个海洋单元格、11 表示一个陆地单元格。
一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 gridgrid 的边界。
返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。
示例 1:
输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]] 输出:3 解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。 复制代码
示例 2:
输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]] 输出:0 解释:所有 1 都在边界上或可以到达边界。 复制代码
提示:
- m == grid.lengthm==grid.length
- n == grid[i].lengthn==grid[i].length
- 1 <= m, n <= 5001<=m,n<=500
- grid[i][j]grid[i][j] 的值为 00 或 11
并查集 + DFS
根据题目定义,我们知道需要统计所有不和「边缘陆地」相连通的「普通陆地」数量。
我们可以用「并查集」来维护连通块,使用 DFS
对所有「边缘陆地连通块」进行标记(设定编号为 00 的超级源点,对于所有的「边缘陆地连通块」,将其与超级源点联通)。
具体的,我们按照如下流程进行处理:
- 初始化并查集:起始让每个单元格独立作为一个连通块;
- 使用
DFS
标记所有「边缘陆地连通块」:从位于边缘的「边缘陆地」进行出发,将其所在连通块与超级源点 00 进行联通标记(同时为了确保复杂度,我们在进行DFS
前需要先检查当前陆地与超级源点的联通关系,如果已联通,说明当前陆地棣属于之前的某个连通块,已被整体标记过,进行跳过即可); - 统计答案:遍历整个棋盘,统计所有不与超级源点 00 联通的陆地数量。
一些细节:由于我们人为规定了超级源点编号为 00,同时棋盘下标从 00 开始,因此对某个点 (x, y)(x,y) 的编号,我们需要增加一个偏移量,例如 idx = x * n + y + 1idx=x∗n+y+1。
代码:
class Solution { int N = 550; int[] p = new int[N * N]; int m, n; int[][] g; int find(int x) { if (p[x] != x) p[x] = find(p[x]); return p[x]; } boolean query(int a, int b) { return find(a) == find(b); } void union(int a, int b) { p[find(a)] = find(b); } public int numEnclaves(int[][] grid) { g = grid; m = g.length; n = g[0].length; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { p[getIdx(i, j)] = getIdx(i, j); } } for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (i == 0 || j == 0 || i == m - 1 || j == n - 1) { if (g[i][j] != 1 || query(getIdx(i, j), 0)) continue; dfs(i, j); } } } int ans = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (g[i][j] == 1 && !query(getIdx(i, j), 0)) ans++; } } return ans; } int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; void dfs(int x, int y) { union(getIdx(x, y), 0); for (int[] d : dirs) { int nx = x + d[0], ny = y + d[1]; if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue; if (g[nx][ny] != 1 || query(getIdx(nx, ny), 0)) continue; dfs(nx, ny); } } int getIdx(int x, int y) { return x * n + y + 1; } } 复制代码
- 时间复杂度:初始化并查集复杂度为 O(m * n)O(m∗n);使用
DFS
对边缘陆地连通块进行标记复杂度为 O(m * n)O(m∗n);统计答案复杂度为 O(m * n)O(m∗n)。整体复杂度为 O(m * n)O(m∗n) - 空间复杂度:O(m * n)O(m∗n)
多源 BFS
也可以使用「多源 BFS
」进行求解。
将所有「边缘陆地」看做与超级源点相连,起始将所有「边缘陆地」进行入队(等价于只将超级源点入队,然后取出超级源点并进行拓展)。
然后是常规的 BFS
过程,所有能够出队/入队的陆地格子,都代表与「边缘陆地」联通,都不属于「飞地」,对其进行标记。
最后遍历整个棋盘,统计所有未被标记的「陆地」格子数量即是答案。
不熟悉「多源 BFS
」的同学可以看前置 🧀 :多源 BFS 入门。
代码:
class Solution { public int numEnclaves(int[][] g) { int m = g.length, n = g[0].length; boolean[][] vis = new boolean[m][n]; Deque<int[]> d = new ArrayDeque<>(); for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (i == 0 || j == 0 || i == m - 1 || j == n - 1) { if (g[i][j] == 0) continue; vis[i][j] = true; d.addLast(new int[]{i, j}); } } } int[][] dirs = new int[][]{{1,0},{-1,0},{0,1},{0,-1}}; while (!d.isEmpty()) { int[] poll = d.pollFirst(); int x = poll[0], y = poll[1]; for (int[] di : dirs) { int nx = x + di[0], ny = y + di[1]; if (nx < 0 || nx >= m || ny < 0 || ny >= n) continue; if (g[nx][ny] != 1) continue; if (vis[nx][ny]) continue; vis[nx][ny] = true; d.addLast(new int[]{nx, ny}); } } int ans = 0; for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (g[i][j] == 1 && !vis[i][j]) ans++; } } return ans; } } 复制代码
- 时间复杂度:O(m * n)O(m∗n)
- 空间复杂度:O(m * n)O(m∗n)
最后
这是我们「刷穿 LeetCode」系列文章的第 No.1011
篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。
在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。
为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:github.com/SharingSour… 。
在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。