这是一个经典的 岛屿的周长(Island Perimeter)问题,属于"网格遍历"和"计数"类型。
核心概念解释
- 周长 (Perimeter): 围绕一个形状的边界的总长度。在本题中,它是组成岛屿的所有陆地单元格与水域(或网格边界)相邻的边的数量总和。
- 陆地 (Land) : g r i d [ i ] [ j ] = 1 grid[i][j] = 1 grid[i][j]=1 的单元格。
- 水域 (Water) : g r i d [ i ] [ j ] = 0 grid[i][j] = 0 grid[i][j]=0 的单元格。
- 相连 (Connected) : 只能通过水平 或垂直方向(上下左右)相邻。
算法思路
对于一个由多个边长为 1 的正方形组成的岛屿,计算其周长,我们不需要进行复杂的图形识别或搜索。我们可以从 局部 的角度来考虑:
对于每一个单独的陆地单元格('1'):
- 一个孤立的陆地单元格的周长是 4。
- 每当一个陆地单元格与另一个相邻的陆地单元格 相连时,它们之间就会有 两条 共用的边(一条属于前者,一条属于后者)。
- 因此,计算总周长的方法可以简化为:
总周长 = (所有陆地单元格的初始周长总和) - (所有相邻陆地单元格共用的边数)
或者,更直观的方法是:
总周长 = 遍历所有陆地单元格,统计每个单元格有多少条边是与水域(或网格边界)相邻的。
具体步骤:
- 初始化周长 : 设定 p e r i m e t e r = 0 perimeter = 0 perimeter=0。
- 遍历网格 : 逐行逐列地检查网格中的每一个单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c]。
- 找到陆地 : 如果当前单元格是 1(陆地),则开始计算它的贡献。
- 检查四个方向 : 检查当前陆地单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 上、下、左、右四个相邻的单元格 g r i d [ n r ] [ n c ] grid[nr][nc] grid[nr][nc]:
- 初始周长贡献为 4。
- 每当一个相邻方向是陆地 ('1') ,就意味着当前单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 在该方向上的一条边被内部消化 了,它不属于周长。因此,该陆地单元格对周长的贡献要 减 1。
- 每当一个相邻方向是水域 ('0') 或超出了网格边界 ,就意味着当前单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 在该方向上的一条边是岛屿的边界 ,它属于周长。因此,该陆地单元格对周长的贡献要 加 1。
由于一个陆地单元格的初始周长是 4,我们只需检查其四个邻居:每有一个邻居是陆地,周长就减 1。
等效简化算法:
对于每一个 g r i d [ r ] [ c ] = 1 grid[r][c] = 1 grid[r][c]=1 的单元格:
- 首先假定它的贡献是 4。
- 检查它的四个邻居:
- 如果邻居 g r i d [ n r ] [ n c ] grid[nr][nc] grid[nr][nc] 是陆地(1)且在网格内 ,则 p e r i m e t e r perimeter perimeter 减 1。
- 累加到总 p e r i m e t e r perimeter perimeter 中。
示例代码(Python)
python
class Solution:
def islandPerimeter(self, grid: list[list[int]]) -> int:
if not grid or not grid[0]:
return 0
rows = len(grid)
cols = len(grid[0])
perimeter = 0
# 定义四个方向的位移
# (dr, dc) 分别代表 (行变化, 列变化): 上, 下, 左, 右
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
# 遍历整个网格
for r in range(rows):
for c in range(cols):
# 只关心陆地单元格
if grid[r][c] == 1:
# 初始周长贡献为 4
current_perimeter = 4
# 检查四个邻居
for dr, dc in directions:
nr, nc = r + dr, c + dc
# 判断邻居是否在网格内
if 0 <= nr < rows and 0 <= nc < cols:
# 如果邻居也是陆地,则共用了一条边,周长减 1
if grid[nr][nc] == 1:
current_perimeter -= 1
# 否则 (邻居在网格外或邻居是水域 '0'),
# 该边都是周长的一部分,保持 current_perimeter 不变
# 因为:
# 1. 邻居在网格外:该边是边界,属于周长
# 2. 邻居是 '0' (水):该边是水边,属于周长
# 累加当前陆地单元格的周长贡献
perimeter += current_perimeter
return perimeter
# 示例 1 测试
grid1 = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
sol = Solution()
print(f"示例 1 的岛屿周长: {sol.islandPerimeter(grid1)}") # 输出: 16
# 示例 2 测试
grid2 = [[1]]
print(f"示例 2 的岛屿周长: {sol.islandPerimeter(grid2)}") # 输出: 4
复杂度分析
- 时间复杂度 : O ( M × N ) O(M \times N) O(M×N)。其中 M M M 是行数, N N N 是列数。我们只需要遍历网格中的每个单元格一次,并在每个单元格上执行固定的 4 次邻居检查。
- 空间复杂度 : O ( 1 ) O(1) O(1)。我们只使用了几个固定的变量来存储周长和方向,没有使用额外的与输入规模相关的存储空间。
python
for dr, dc in directions:
nr, nc = r + dr, c + dc
是 一个坐标层面的循环(也可以叫方向循环)。
dr
、dc
、nr
、nc
都是常用的缩写,它们代表的英文含义如下:
缩写含义解释
缩写 | 英文全称 (English Full Name) | 中文含义 | 作用和背景 |
---|---|---|---|
dr | d elta Row | 行的增量/变化量 | 表示从当前行 r r r 移动到相邻行的行索引变化值。 |
dc | d elta Column | 列的增量/变化量 | 表示从当前列 c c c 移动到相邻列的列索引变化值。 |
nr | N ext Row | 下一个行索引 | 通过 r + d r r + dr r+dr 计算出的相邻单元格的行索引。 |
nc | N ext Column | 下一个列索引 | 通过 c + d c c + dc c+dc 计算出的相邻单元格的列索引。 |
涉及的英文单词解释
-
Delta ( Δ \Delta Δ):
- 词源来历: 源自希腊字母表的第四个字母 Δ \Delta Δ(大写)。在数学和科学中, Δ \Delta Δ 常用作符号,表示变化量 (Change)或差值(Difference)。
- 在代码中: dr \text{dr} dr 和 dc \text{dc} dc 中的 d \text{d} d 就是 Delta \text{Delta} Delta 的缩写,代表行和列索引的变动。
-
Row:
- 含义: 行。在矩阵或表格中,水平排列的一组数据。
- 对应缩写: dr \text{dr} dr 中的 R \text{R} R 和 nr \text{nr} nr 中的 R \text{R} R。
-
Column:
- 含义: 列。在矩阵或表格中,垂直排列的一组数据。
- 对应缩写: dc \text{dc} dc 中的 C \text{C} C 和 nc \text{nc} nc 中的 C \text{C} C。
-
Next:
- 含义: 下一个。表示紧接着当前位置的相邻位置。
- 对应缩写: nr \text{nr} nr 和 nc \text{nc} nc 中的 N \text{N} N。
代码的作用
在算法中,directions
通常定义为:
python
directions = [
(0, 1), # 右 (dc = +1)
(0, -1), # 左 (dc = -1)
(1, 0), # 下 (dr = +1)
(-1, 0) # 上 (dr = -1)
]
循环的作用就是:通过遍历每一个 dr \text{dr} dr 和 dc \text{dc} dc 组合 ,计算出当前点 ( r , c ) (r, c) (r,c) 的下一个相邻点 ( n r , n c ) (nr, nc) (nr,nc)。
🧭 一、背景:我们在一个网格中
假设有一个 3×3 的网格:
c=0 | c=1 | c=2 | |
---|---|---|---|
r=0 | (0,0) | (0,1) | (0,2) |
r=1 | (1,0) | (1,1) | (1,2) |
r=2 | (2,0) | (2,1) | (2,2) |
每个格子都有一个"坐标" (r, c)
。
🧮 二、定义四个方向
在代码中:
python
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
这四个方向是相对坐标变化量,可以理解成"偏移量 (delta)":
方向 | dr | dc | 含义 |
---|---|---|---|
(0, 1) | 0 | +1 | 向右移动一格 |
(0, -1) | 0 | -1 | 向左移动一格 |
(1, 0) | +1 | 0 | 向下移动一格 |
(-1, 0) | -1 | 0 | 向上移动一格 |
🔁 三、开始循环
当我们在一个格子,比如 (r, c) = (1, 1)
时,
执行:
python
for dr, dc in directions:
nr, nc = r + dr, c + dc
这个循环会依次产生 4 组"邻居坐标"👇
dr, dc | 计算 nr, nc = r+dr, c+dc | 结果 | 方向 |
---|---|---|---|
(0, 1) | (1+0, 1+1) | (1, 2) | 右边 |
(0, -1) | (1+0, 1-1) | (1, 0) | 左边 |
(1, 0) | (1+1, 1+0) | (2, 1) | 下边 |
(-1, 0) | (1-1, 1+0) | (0, 1) | 上边 |
🧠 四、逻辑理解
这段循环做的事情是:
「从当前格子出发,把上下左右四个邻居都找出来,逐个检查它们的状态(水还是陆地?是否越界?)」
🔍 五、举个小例子
假设当前 (r, c) = (0, 0)
,位于左上角:
dr, dc | nr, nc | 是否越界 | 说明 |
---|---|---|---|
(0, 1) | (0, 1) | ✅ 在网格内 | 右边 |
(0, -1) | (0, -1) | ❌ 越界 | 左边没有格子 |
(1, 0) | (1, 0) | ✅ 在网格内 | 下边 |
(-1, 0) | (-1, 0) | ❌ 越界 | 上边没有格子 |
程序会判断:
python
if 0 <= nr < rows and 0 <= nc < cols:
# 邻居在网格内
🧩 六、总结一句话
✅
for dr, dc in directions
是在循环四个方向的偏移量 ,✅
nr, nc = r + dr, c + dc
是计算邻居的坐标。它的作用就是:从当前格子出发,访问上下左右四个相邻格子。
💡 记忆方法:
你可以想象成:
"我站在 (r, c) 这个格子里,看四周四个方向(上、下、左、右),
每看一个方向,就把那个格子的坐标算出来 ------ 这就是 (r+dr, c+dc)。"
代码的"直觉理解":每个陆地格子都"自带 4 条边"
我们先假设有一个陆地格子:
1
- 如果它孤立存在 (周围全是水或边界),它有 4 条边;
- 如果它的右边也有陆地,那这条右边就"合并"了,不再算边;
- 同理,上下左右凡是有邻居为 1,就各减去 1 条边。
🔹 举例:
假设这是一个岛:
1 1
1 1
每个小格都自带 4 条边,一共 4×4=16
但中间共用的边要减掉:
共享边方向 | 数量 | 每条边重复算了两次 | 实际要减去几条 |
---|---|---|---|
水平相邻 | 2 | 每对格子共用 1 条边 | 2 |
垂直相邻 | 2 | 每对格子共用 1 条边 | 2 |
所以:
总周长 = 16 - 2 - 2 = 12
🧩 二、代码逻辑和思路
让我们配合伪代码拆一下逻辑:
python
if grid[r][c] == 1: # 如果是陆地
current_perimeter = 4 # 每个陆地初始有4条边
for dr, dc in directions: # 检查四个方向(上、下、左、右)
nr, nc = r + dr, c + dc
if 0 <= nr < rows and 0 <= nc < cols: # 邻居在网格内
if grid[nr][nc] == 1: # 邻居也是陆地
current_perimeter -= 1 # 共用一条边 → 减1
perimeter += current_perimeter # 加到总周长
🔁 这意味着:
- 你遍历所有陆地;
- 每个陆地默认有 4 条边;
- 检查四周:每遇到一个"陆地邻居",就少 1 条边;
- 全部格子算完后,所有边都正确统计到。
计算个体贡献,然后累加到总体。
1. 局部计算:current_perimeter -= 1
这行代码发生在 for dr, dc in directions:
循环内部 ,用于确定一个陆地单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 有多少条边是内接(被相邻的陆地单元格抵消)的。
- 逻辑 : 当 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 的一个邻居 g r i d [ n r ] [ n c ] grid[nr][nc] grid[nr][nc] 也是陆地 ('1') 时,它们之间有一条共用的边。这条边不能算作岛屿的周长。
- 操作 : 我们最初假定 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 的周长贡献是 4 4 4。每发现一条共用的边,就将这个局部周长
current_perimeter
减去 1 1 1。
总结: current_perimeter
是在检查完所有四个邻居后,当前单元格 ( r , c ) (r, c) (r,c) 对周长的实际贡献值。
2. 全局累加:perimeter += current_perimeter
这行代码发生在 for r in range(rows):
和 for c in range(cols):
的循环内部 ,但位于检查当前单元格 ( r , c ) (r, c) (r,c) 的所有邻居的外部 (即在 for dr, dc in directions:
循环结束后)。
- 逻辑 : 完成对单元格 g r i d [ r ] [ c ] grid[r][c] grid[r][c] 的四个邻居的检查和
current_perimeter
的计算后,我们就得到了这个单元格对总周长的最终贡献。 - 操作 : 将这个个体贡献值
current_perimeter
累加 到存储总周长的变量perimeter
上。
总结: perimeter
是所有陆地单元格的实际周长贡献的总和,是最终的答案。
关系图解
- 初始化 : g r i d [ r ] [ c ] = 1 grid[r][c] = 1 grid[r][c]=1 时,
current_perimeter = 4
。 - 局部处理( 4 4 4 次循环) :
- IF 邻居是陆地:
current_perimeter = current_perimeter - 1
- ELSE 邻居是水/边界:
current_perimeter
不变
- IF 邻居是陆地:
- 结果 : 循环结束后,
current_perimeter
可能是 4 , 3 , 2 , 1 4, 3, 2, 1 4,3,2,1 或 0 0 0。 - 全局累加 : p e r i m e t e r = p e r i m e t e r + c u r r e n t _ p e r i m e t e r perimeter = perimeter + current\_perimeter perimeter=perimeter+current_perimeter。
这两行代码体现了"分而治之"的思想:先独立计算每个陆地格子的净周长贡献,再将所有贡献相加得到整个岛屿的总周长。
在这个内部的 for
循环中,current_perimeter
最多只能减少 4 4 4 次。**
详细解释
-
初始值(最多 4 4 4)
current_perimeter = 4
: 算法基于一个基本事实------每个边长为 1 的正方形(陆地单元格)最多 只有 4 4 4 条边。
-
循环次数(固定 4 4 4 次)
for dr, dc in directions:
:directions
列表(通常是[(0, 1), (0, -1), (1, 0), (-1, 0)]
)只有 4 4 4 个元素,代表上、下、左、右 4 4 4 个固定的方向。因此,这个for
循环只执行 4 4 4 次。
-
减少条件(最多 4 4 4 次满足)
if grid[nr][nc] == 1:
: 每次循环中,只有当满足两个条件时才会执行current_perimeter -= 1
:- 邻居在网格内部(
0 <= nr < rows and 0 <= nc < cols
)。 - 邻居是陆地(
grid[nr][nc] == 1
)。
- 邻居在网格内部(
- 因为循环只执行 4 4 4 次,所以这个减少操作
current_perimeter -= 1
也最多只能被执行 4 4 4 次。
极端情况举例
陆地单元格位置 | 邻居 ('1') 个数 | current_perimeter 变化 |
最终 current_perimeter |
意义 |
---|---|---|---|---|
孤立点 (周围都是 '0' 或边界) | 0 | 4 - 0 = 4 | 4 | 贡献了 4 4 4 条边到周长 |
角点 (被 2 2 2 个陆地邻居包围) | 2 | 4 - 2 = 2 | 2 | 贡献了 2 2 2 条边到周长 |
边点 (被 3 3 3 个陆地邻居包围) | 3 | 4 - 3 = 1 | 1 | 贡献了 1 1 1 条边到周长 |
中心点 (被 4 4 4 个陆地邻居包围) | 4 | 4 - 4 = 0 | 0 | 贡献了 0 0 0 条边到周长 |
因此,这个算法设计的核心正是基于每个方格最多 4 4 4 条边 的事实,通过 4 4 4 次固定的检查来准确计算其净贡献。
🧩 回顾:整体框架
你可以先记住,这段代码有三层逻辑:
1️⃣ 外层两重 for 循环 :遍历每个格子
2️⃣ 内层 for 循环 :检查当前格子的四个方向(上、下、左、右)
3️⃣ 判断条件 :
→ 如果邻居是陆地,就把当前格子的边数减 1
🧱 举个例子(直观)
我们用一个 3×3 的小网格来"跑一遍":
python
grid = [
[0, 1, 0],
[1, 1, 1],
[0, 1, 0]
]
岛屿长这样:
010
111
010
第一步:定义方向
python
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
这 4 个方向可以让我们从当前格子 (r, c) 移动到四个相邻格子 (nr, nc)。
第二步:外层循环(从左上角开始扫描)
程序执行顺序:
python
for r in range(rows): # 一行一行
for c in range(cols): # 每行从左到右
它依次访问这些格子:
(0,0) → (0,1) → (0,2) → (1,0) → (1,1) → (1,2) → (2,0) → (2,1) → (2,2)
🔍 第三步:当遇到陆地时(grid[r][c] == 1)
我们重点看几个格子的过程👇:
🟩 例子 1:格子 (0,1)
它是陆地(grid[0][1] == 1
),所以开始执行:
python
current_perimeter = 4
默认它有 4 条边。
然后依次检查四个方向:
方向 | 计算 nr, nc | grid[nr][nc] 值 | 动作 |
---|---|---|---|
右 (0,1) | (0,2) | 0 | 水,不减 |
左 (0,-1) | 越界 | 不存在 | 边界,不减 |
下 (1,1) | 1 | 陆地 → -1 | |
上 (-1,1) | 越界 | 不存在 | 边界,不减 |
🔹 结果:
current_perimeter = 4 - 1 = 3
→ 当前格子的周长贡献 = 3
🟩 例子 2:格子 (1,1)
这是中间的格子,四面都有邻居:
方向 | 计算 nr, nc | grid[nr][nc] 值 | 动作 |
---|---|---|---|
右 | (1,2) | 1 | -1 |
左 | (1,0) | 1 | -1 |
下 | (2,1) | 1 | -1 |
上 | (0,1) | 1 | -1 |
🔹 结果:
current_perimeter = 4 - 4 = 0
→ 它完全被包围,不贡献周长。
🟩 例子 3:格子 (2,1)
方向 | 计算 nr, nc | grid[nr][nc] 值 | 动作 |
---|---|---|---|
右 | (2,2) | 0 | 水,不减 |
左 | (2,0) | 0 | 水,不减 |
下 | (3,1) | 越界 | 边界,不减 |
上 | (1,1) | 1 | -1 |
🔹 结果:
current_perimeter = 4 - 1 = 3
📊 第四步:累加结果
每遇到一个陆地格子:
python
perimeter += current_perimeter
程序会把每个陆地的"贡献周长"都加起来。
最后返回:
python
return perimeter
总结运行逻辑图(文字版)
for 每一行
for 每一列
if 当前格是陆地:
current_perimeter = 4
for 四个方向:
if 邻居在范围内 且 是陆地:
current_perimeter -= 1
总周长 += current_perimeter
return 总周长