统计子岛屿 图解题解
这道题到底在问什么
- 输入
- grid1=[[1,1,0,0],[0,1,0,0],[0,0,0,1],[1,1,1,1]], grid2=[[1,1,1,0],[0,1,0,0],[0,0,0,1],[1,1,0,1]]
- 输出
- 2
- 输入
- grid1=[[1]], grid2=[[1]]
- 输出
- 1
最优解:为什么这么做
一句话答案:LeetCode 1905 统计子岛屿:在 grid2 上逐块 DFS 沉岛数岛,顺路把每格在 grid1 的值按位与起来,一整块岛都踩着陆地才算子岛屿,只要夹一个水洞就作废,时间 O(m·n)。
grid2 里哪些岛被 grid1 的陆地整块托住
两张一样大的网格 grid1 和 grid2,只有 0 和 1,1 是陆地、0 是水,上下左右连成一片的陆地就是一座岛屿。grid2 的某座岛屿,若它盖住的每一格在 grid1 里也全是陆地,就叫子岛屿,要数的正是这种岛有几座。题面 grid2=[[1,1,1,0],[0,1,0,0],[0,0,0,1],[1,1,0,1]] 配 grid1=[[1,1,0,0],[0,1,0,0],[0,0,0,1],[1,1,1,1]],答案是 2。
把两张图的岛一一配对去比,对不上
拿 grid2 的岛和 grid1 的岛一对一比形状,立刻卡住:子岛屿根本不要求两边是同一座岛的同一形状,只要 grid2 这块盖住的格子在 grid1 都踩着陆地就行。grid1 的一整片陆地里就能装下 grid2 的好几座小岛,按整岛配对从一开始就问错了问题。
改成按格判托底,一块岛只问一个问题
把问题缩到一格:grid2 这块岛里,有没有哪一格在 grid1 是水?一个都没有,这块就过关。走遍一整块岛用 DFS,也就是踩着相连的陆地一格接一格递归铺开;为了不把同一块岛数第二遍,走过的 grid2 格子随手改成 0 沉掉,这叫沉岛,省下另开一张标记表。沿路给这块岛记一个 ok,起点取它在 grid1 的值,每多踩一格就和那格的 grid1 值按位与一下——碰上 0,ok 就归 0,这块岛作废。
外层扫 grid2,踩了洞也得把整块走完
外层循环从左上角逐格扫 grid2,碰到还没沉的陆地就以它为起点发起 DFS,把整块岛走遍,DFS 的返回值就是这块岛的 ok,累加进答案。这里有个反直觉处:某格发现 grid1 是水、ok 已归 0,也绝不能当场收手返回。剩下的陆地还得走遍沉掉,不然这些残格会被外层当成没碰过的新岛屿,再数一遍,答案偏大。
拿题面这两张网格逐块搜一趟
外层先撞上 (0,0),发起第一次 DFS。起点 grid1 是 1,ok 记成 1;按上右下左的顺序钻到 (0,1) 还是 1,再钻到 (0,2)——它在 grid1 是 0,ok 当场压成 0;可 DFS 不收手,回头把 (1,1) 也走遍沉掉。这块 4 格岛因为 (0,2) 的洞,ok 收成 0,不加。
外层接着走,沉掉的格子全跳过,撞上 (2,3) 连到 (3,3),两格在 grid1 都是 1,ok 全程是 1,加到 1;再撞上 (3,0) 连到 (3,1),两格也都托底,ok 又是 1,加到 2。三座岛查完,答案 2。
最容易漏沉、错扫的那几处
另一个易错点是外层别错扫 grid1 的陆地——起点和连通都以 grid2 为准,grid1 只当那把量托底的尺子。边界上,两张网格都只有一格陆地且都是 1 时答案是 1;grid2 陆地而 grid1 是水的格子就是个洞。时间上外层扫一遍、每个陆地格只被 DFS 沉一次,合起来 O(m·n);空间按峰值算,原地沉岛不另开表,递归栈最坏排到格子总数量级,同样 O(m·n)。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3记牢这一句:grid2 的一座岛屿要成为子岛屿,岛内每一个格子在 grid1 里都必须是陆地,一个都不能踩空。下面从左上角开始,一座一座岛屿地查。
- 4底图:哪里有陆地这是 grid1,把它当成一张大陆底图。绿色是陆地 1,浅蓝是水 0。它本身不数岛屿,它的作用是当一把尺子:grid2 的岛屿想过关,岛内每个格子都得落在 grid1 的绿色陆地上。你先记住它绿色陆地的分布,左上角有一小片,右下角整行连成一片。
- 5主角:数子岛屿这是 grid2,它才是我们要数岛屿的主角。绿色陆地一眼看去分成三块:左上角一块 4 格,右下角一块 2 格,左下角一块 2 格。接下来的活儿就是:把这三块岛屿逐一拿去和 grid1 比对,看哪些整块都被 grid1 的陆地托住。
- 6目标:数出子岛屿为了少画一张图,把两张网格合到一起看。舞台这张网格的形状就是 grid2:凡是 grid2 的陆地格,我在上面直接写它在 grid1 的托底值,1 表示底下有陆地稳稳托着,0 表示底下是水、是个洞;grid2 的水格写一个点,不参与数岛。左上那块里 (0,2) 写着 0,已经能看出它脚下踩空,不过我们仍按算法一步步走过去确认。
- 7第一座岛屿开工外层循环从左上角开始扫 grid2,第一个碰到的陆地是 (0,0),以它为起点发起一次 DFS,把整座岛屿走遍。约定一个记号 ok,一路把岛内每格在 grid1 的值按位与进去,起点这格 grid1 是 1,ok 先记成 1。
- 8进入 (0,0)正式踏上起点 (0,0),把它沉掉防止回头重数。低头看脚下写的数字是 1,说明它在 grid1 也是陆地,托底没问题。接着按上右下左四个方向,找相连的陆地继续往里钻。
- 9沿右侧深入站在 (0,0),按上右下左依次看四个邻居。上边出界,右边 (0,1) 是陆地、还没走过,那就先钻进去。DFS 的规矩就是这样:每到一格,先把一条路走到底,再回头处理别的分支。
- 10进入 (0,1)沿着相连的陆地走到 (0,1),把它沉掉。脚下写着 1,在 grid1 里也是陆地,这一格托底稳当。到目前为止这座岛屿每一格都踩在陆地上,ok 依然是 1。
- 11先探右邻来到 (0,1),它有两条路可走:右边 (0,2) 是陆地,下边 (1,1) 也是陆地。按上右下左的顺序,先钻右边那条,把它彻底走完再回来处理下边。正是这一步之后,我们会撞上那个藏在右边的洞。
- 12(0,2) 是洞顺着相连陆地走到 (0,2),低头一看脚下写着 0,这个位置在 grid1 是水。按位与遇到 0,ok 立刻被压成 0,这座岛屿从此注定不是子岛屿。但注意:算法不会马上收手,还要把这块剩下的陆地都走遍沉掉,免得残格被后面误当成新岛屿。
- 13进入 (1,1)沿着相连的陆地走到 (1,1),把它沉掉。脚下写着 1,在 grid1 里也是陆地,这一格托底稳当。不过之前已经踩过洞了,ok 已经是 0,这一格是 1 也救不回来,继续把剩余格子走完。
- 14作废,不计数这座岛屿一共 4 个格子,可惜其中 (0,2) 那格在 grid1 是水,ok 早被压成了 0。哪怕别的格子都托底,只要有这一个洞,整块就不算子岛屿。把它标成灰色、洞那格标红,已确认子岛屿数仍然是 0,不加。
- 15第二座岛屿开工外层扫描继续往后走,前面走过的格子都已经沉掉了,现在碰到还没处理的陆地 (2,3),这是第二座岛屿的起点,又发起一次 DFS。ok 重新从起点的 grid1 值 1 开始。
- 16进入 (2,3)正式踏上起点 (2,3),把它沉掉防止回头重数。低头看脚下写的数字是 1,说明它在 grid1 也是陆地,托底没问题。接着按上右下左四个方向,找相连的陆地继续往里钻。
- 17进入 (3,3)沿着相连的陆地走到 (3,3),把它沉掉。脚下写着 1,在 grid1 里也是陆地,这一格托底稳当。到目前为止这座岛屿每一格都踩在陆地上,ok 依然是 1。
- 18子岛屿数 = 1这座岛屿走完了,一共 2 个格子,每一个在 grid1 里都是陆地,ok 始终是 1。它就是一个子岛屿,把它整块标成绿色,已确认子岛屿数加到 1。
- 19第三座岛屿开工外层扫描继续往后走,前面走过的格子都已经沉掉了,现在碰到还没处理的陆地 (3,0),这是第三座岛屿的起点,又发起一次 DFS。ok 重新从起点的 grid1 值 1 开始。
- 20进入 (3,0)正式踏上起点 (3,0),把它沉掉防止回头重数。低头看脚下写的数字是 1,说明它在 grid1 也是陆地,托底没问题。接着按上右下左四个方向,找相连的陆地继续往里钻。
- 21进入 (3,1)沿着相连的陆地走到 (3,1),把它沉掉。脚下写着 1,在 grid1 里也是陆地,这一格托底稳当。到目前为止这座岛屿每一格都踩在陆地上,ok 依然是 1。
- 22子岛屿数 = 2这座岛屿走完了,一共 2 个格子,每一个在 grid1 里都是陆地,ok 始终是 1。它就是一个子岛屿,把它整块标成绿色,已确认子岛屿数加到 2。
- 23答案 = 2三座岛屿全部查完。左上那块 4 格因为 (0,2) 踩了洞作废;右下和左下两块,每个格子都稳稳落在 grid1 的陆地上,是货真价实的子岛屿。数一数绿色的岛,一共 2 座,这就是答案。判定始终只看一条:岛内有没有哪个格子在 grid1 里是水。
⚠️ 容易写错的地方
✗ 错:一发现某格在 grid1 是水就立刻 return
✓ 对:标记 ok 为 0,但仍把整块岛屿走遍沉掉
提前返回会漏沉这块岛屿剩下的格子,它们会被外层循环当成新岛屿重复统计,答案偏大
✗ 错:把 grid2 岛屿和 grid1 岛屿一一配对再比较
✓ 对:只需岛内每格在 grid1 都是陆地即可
子岛屿只要求被 grid1 的陆地完全覆盖,不要求两边是同一座岛屿的同一形状,按格判断更简单也更准
✗ 错:另开一张 visited 表记录访问
✓ 对:直接把走过的 grid2 格子置 0
题目允许原地修改,把陆地沉成水就是天然的访问标记,省一张 m 乘 n 的表
✗ 错:外层对 grid1 的陆地发起搜索
✓ 对:外层必须扫 grid2 的陆地
我们数的是 grid2 里的岛屿,起点和连通都以 grid2 为准,grid1 只在判断托底时被查值
完整代码(Python / C++ / Java)
Python
from __future__ import annotations
from typing import *
from collections import *
from functools import *
from itertools import *
from math import *
from heapq import *
from bisect import *
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class ListNode:
def __init__(self, val=0, next=None):
self.val = val
self.next = next
class Solution:
def countSubIslands(self, grid1: List[List[int]], grid2: List[List[int]]) -> int:
def dfs(i: int, j: int) -> int:
ok = grid1[i][j]
grid2[i][j] = 0
for a, b in pairwise(dirs):
x, y = i + a, j + b
if 0 <= x < m and 0 <= y < n and grid2[x][y] and not dfs(x, y):
ok = 0
return ok
m, n = len(grid1), len(grid1[0])
dirs = (-1, 0, 1, 0, -1)
return sum(dfs(i, j) for i in range(m) for j in range(n) if grid2[i][j])C++
#include <algorithm>
#include <array>
#include <cctype>
#include <climits>
#include <cmath>
#include <deque>
#include <functional>
#include <iostream>
#include <limits>
#include <map>
#include <numeric>
#include <optional>
#include <queue>
#include <set>
#include <sstream>
#include <stack>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <vector>
using namespace std;
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
int m = grid1.size(), n = grid1[0].size();
int ans = 0;
int dirs[5] = {-1, 0, 1, 0, -1};
function<int(int, int)> dfs = [&](int i, int j) {
int ok = grid1[i][j];
grid2[i][j] = 0;
for (int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && grid2[x][y]) {
ok &= dfs(x, y);
}
}
return ok;
};
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid2[i][j]) {
ans += dfs(i, j);
}
}
}
return ans;
}
};Java
import java.util.*;
class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(){} TreeNode(int val){this.val=val;} TreeNode(int val, TreeNode left, TreeNode right){this.val=val;this.left=left;this.right=right;} }
class ListNode { int val; ListNode next; ListNode(){} ListNode(int val){this.val=val;} ListNode(int val, ListNode next){this.val=val;this.next=next;} }
class Solution {
private final int[] dirs = {-1, 0, 1, 0, -1};
private int[][] grid1;
private int[][] grid2;
private int m;
private int n;
public int countSubIslands(int[][] grid1, int[][] grid2) {
m = grid1.length;
n = grid1[0].length;
this.grid1 = grid1;
this.grid2 = grid2;
int ans = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid2[i][j] == 1) {
ans += dfs(i, j);
}
}
}
return ans;
}
private int dfs(int i, int j) {
int ok = grid1[i][j];
grid2[i][j] = 0;
for (int k = 0; k < 4; ++k) {
int x = i + dirs[k], y = j + dirs[k + 1];
if (x >= 0 && x < m && y >= 0 && y < n && grid2[x][y] == 1) {
ok &= dfs(x, y);
}
}
return ok;
}
}复杂度
时间
O(m·n)
m 行 n 列共 m 乘 n 个格子。外层扫一遍每个格子,每个陆地格被某次 DFS 恰好访问并沉掉一次,每格只看四个固定方向,都是常数操作,整体随格子总数线性增长
空间
O(m·n)
按峰值算。原地把 grid2 当访问标记不额外开表;递归栈深度最坏是一整块岛屿铺满整张网格时,栈里排到接近 m 乘 n 个格子。峰值就是 O(m·n),不随面积平方增长
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 统计子岛屿 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
发现某格在 grid1 是水,为什么不能立刻 return?+
因为这块岛剩下的陆地还没沉掉。DFS 一旦提前返回,同一座岛里没走到的格子仍是 1,外层循环扫过来会把它们当成一座没碰过的新岛屿,又发起一次 DFS 数一遍,答案就偏大。正确做法是把 ok 标成 0 记下这块岛作废,但照样把整块岛走遍、每格都置 0 沉掉。判定作废和沉岛清场是两件事,不能因为前者省掉后者。
为什么外层扫的是 grid2,不是 grid1?+
我们数的是 grid2 里的子岛屿,岛屿的起点、连通关系全以 grid2 为准,grid1 只在判断托底时被查一下值。要是外层去扫 grid1 的陆地发起搜索,数的就成了 grid1 自己的岛,跟题目要的南辕北辙。记法很简单:谁的岛就扫谁,grid1 全程只当那把量托底的尺子,不参与找岛。
除了 DFS 沉岛,还有别的写法吗?+
有。可以先在 grid1 上给每座岛编个号,再遍历 grid2 的每座岛,看它盖住的格子在 grid1 里是不是都落在陆地上;也能用并查集,也就是把相邻同为陆地的格子并成一堆,再逐块检查覆盖关系。这几种复杂度都是格子数量级 O(m·n),其中 DFS 洪水填充边走边沉、边按位与,写起来最直接,不用额外维护编号或集合。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 统计子岛屿 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。