【小白笔记】岛屿的周长(Island Perimeter)
这是一个经典的 岛屿的周长(Island Perimeter)问题,属于“网格遍历”和“计数”类型。
核心概念解释
- 周长 (Perimeter): 围绕一个形状的边界的总长度。在本题中,它是组成岛屿的所有陆地单元格与水域(或网格边界)相邻的边的数量总和。
- 陆地 (Land): grid[i][j]=1grid[i][j] = 1grid[i][j]=1 的单元格。
- 水域 (Water): grid[i][j]=0grid[i][j] = 0grid[i][j]=0 的单元格。
- 相连 (Connected): 只能通过水平或垂直方向(上下左右)相邻。
算法思路
对于一个由多个边长为 1 的正方形组成的岛屿,计算其周长,我们不需要进行复杂的图形识别或搜索。我们可以从 局部 的角度来考虑:
对于每一个单独的陆地单元格(‘1’):
- 一个孤立的陆地单元格的周长是 4。
- 每当一个陆地单元格与另一个相邻的陆地单元格相连时,它们之间就会有 两条 共用的边(一条属于前者,一条属于后者)。
- 因此,计算总周长的方法可以简化为:
总周长 = (所有陆地单元格的初始周长总和) - (所有相邻陆地单元格共用的边数)
或者,更直观的方法是:
总周长 = 遍历所有陆地单元格,统计每个单元格有多少条边是与水域(或网格边界)相邻的。
具体步骤:
- 初始化周长: 设定 perimeter=0perimeter = 0perimeter=0。
- 遍历网格: 逐行逐列地检查网格中的每一个单元格 grid[r][c]grid[r][c]grid[r][c]。
- 找到陆地: 如果当前单元格是 1(陆地),则开始计算它的贡献。
- 检查四个方向: 检查当前陆地单元格 grid[r][c]grid[r][c]grid[r][c] 上、下、左、右四个相邻的单元格 grid[nr][nc]grid[nr][nc]grid[nr][nc]:
- 初始周长贡献为 4。
- 每当一个相邻方向是陆地 (‘1’),就意味着当前单元格 grid[r][c]grid[r][c]grid[r][c] 在该方向上的一条边被内部消化了,它不属于周长。因此,该陆地单元格对周长的贡献要 减 1。
- 每当一个相邻方向是水域 (‘0’) 或超出了网格边界,就意味着当前单元格 grid[r][c]grid[r][c]grid[r][c] 在该方向上的一条边是岛屿的边界,它属于周长。因此,该陆地单元格对周长的贡献要 加 1。
由于一个陆地单元格的初始周长是 4,我们只需检查其四个邻居:每有一个邻居是陆地,周长就减 1。
等效简化算法:
对于每一个 grid[r][c]=1grid[r][c] = 1grid[r][c]=1 的单元格:
- 首先假定它的贡献是 4。
- 检查它的四个邻居:
- 如果邻居 grid[nr][nc]grid[nr][nc]grid[nr][nc] 是陆地(1)且在网格内,则 perimeterperimeterperimeter 减 1。
- 累加到总 perimeterperimeterperimeter 中。
示例代码(Python)
class Solution:def islandPerimeter(self, grid: list[list[int]]) -> int:if not grid or not grid[0]:return 0rows = 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:# 初始周长贡献为 4current_perimeter = 4# 检查四个邻居for dr, dc in directions:nr, nc = r + dr, c + dc# 判断邻居是否在网格内if 0 <= nr < rows and 0 <= nc < cols:# 如果邻居也是陆地,则共用了一条边,周长减 1if grid[nr][nc] == 1:current_perimeter -= 1# 否则 (邻居在网格外或邻居是水域 '0'),# 该边都是周长的一部分,保持 current_perimeter 不变# 因为:# 1. 邻居在网格外:该边是边界,属于周长# 2. 邻居是 '0' (水):该边是水边,属于周长# 累加当前陆地单元格的周长贡献perimeter += current_perimeterreturn 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)。其中 MMM 是行数,NNN 是列数。我们只需要遍历网格中的每个单元格一次,并在每个单元格上执行固定的 4 次邻居检查。
- 空间复杂度:O(1)O(1)O(1)。我们只使用了几个固定的变量来存储周长和方向,没有使用额外的与输入规模相关的存储空间。
for dr, dc in directions:nr, nc = r + dr, c + dc
是 一个坐标层面的循环(也可以叫方向循环)。
dr
、dc
、nr
、nc
都是常用的缩写,它们代表的英文含义如下:
缩写含义解释
缩写 | 英文全称 (English Full Name) | 中文含义 | 作用和背景 |
---|---|---|---|
dr | delta Row | 行的增量/变化量 | 表示从当前行 rrr 移动到相邻行的行索引变化值。 |
dc | delta Column | 列的增量/变化量 | 表示从当前列 ccc 移动到相邻列的列索引变化值。 |
nr | Next Row | 下一个行索引 | 通过 r+drr + drr+dr 计算出的相邻单元格的行索引。 |
nc | Next Column | 下一个列索引 | 通过 c+dcc + dcc+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
通常定义为:
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) 的下一个相邻点 (nr,nc)(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)
。
🧮 二、定义四个方向
在代码中:
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)
时,
执行:
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) | ❌ 越界 | 上边没有格子 |
程序会判断:
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
🧩 二、代码逻辑和思路
让我们配合伪代码拆一下逻辑:
if grid[r][c] == 1: # 如果是陆地current_perimeter = 4 # 每个陆地初始有4条边for dr, dc in directions: # 检查四个方向(上、下、左、右)nr, nc = r + dr, c + dcif 0 <= nr < rows and 0 <= nc < cols: # 邻居在网格内if grid[nr][nc] == 1: # 邻居也是陆地current_perimeter -= 1 # 共用一条边 → 减1perimeter += current_perimeter # 加到总周长
🔁 这意味着:
- 你遍历所有陆地;
- 每个陆地默认有 4 条边;
- 检查四周:每遇到一个“陆地邻居”,就少 1 条边;
- 全部格子算完后,所有边都正确统计到。
计算个体贡献,然后累加到总体。
1. 局部计算:current_perimeter -= 1
这行代码发生在 for dr, dc in directions:
循环内部,用于确定一个陆地单元格 grid[r][c]grid[r][c]grid[r][c] 有多少条边是内接(被相邻的陆地单元格抵消)的。
- 逻辑: 当 grid[r][c]grid[r][c]grid[r][c] 的一个邻居 grid[nr][nc]grid[nr][nc]grid[nr][nc] 也是陆地 (‘1’) 时,它们之间有一条共用的边。这条边不能算作岛屿的周长。
- 操作: 我们最初假定 grid[r][c]grid[r][c]grid[r][c] 的周长贡献是 444。每发现一条共用的边,就将这个局部周长
current_perimeter
减去 111。
总结: 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:
循环结束后)。
- 逻辑: 完成对单元格 grid[r][c]grid[r][c]grid[r][c] 的四个邻居的检查和
current_perimeter
的计算后,我们就得到了这个单元格对总周长的最终贡献。 - 操作: 将这个个体贡献值
current_perimeter
累加到存储总周长的变量perimeter
上。
总结: perimeter
是所有陆地单元格的实际周长贡献的总和,是最终的答案。
关系图解
- 初始化: grid[r][c]=1grid[r][c] = 1grid[r][c]=1 时,
current_perimeter = 4
。 - 局部处理(444 次循环):
- IF 邻居是陆地:
current_perimeter = current_perimeter - 1
- ELSE 邻居是水/边界:
current_perimeter
不变
- IF 邻居是陆地:
- 结果: 循环结束后,
current_perimeter
可能是 4,3,2,14, 3, 2, 14,3,2,1 或 000。 - 全局累加: perimeter=perimeter+current_perimeterperimeter = perimeter + current\_perimeterperimeter=perimeter+current_perimeter。
这两行代码体现了“分而治之”的思想:先独立计算每个陆地格子的净周长贡献,再将所有贡献相加得到整个岛屿的总周长。
在这个内部的 for
循环中,current_perimeter
最多只能减少 444 次。**
详细解释
-
初始值(最多 444)
current_perimeter = 4
: 算法基于一个基本事实——每个边长为 1 的正方形(陆地单元格)最多只有 444 条边。
-
循环次数(固定 444 次)
for dr, dc in directions:
:directions
列表(通常是[(0, 1), (0, -1), (1, 0), (-1, 0)]
)只有 444 个元素,代表上、下、左、右 444 个固定的方向。因此,这个for
循环只执行 444 次。
-
减少条件(最多 444 次满足)
if grid[nr][nc] == 1:
: 每次循环中,只有当满足两个条件时才会执行current_perimeter -= 1
:- 邻居在网格内部(
0 <= nr < rows and 0 <= nc < cols
)。 - 邻居是陆地(
grid[nr][nc] == 1
)。
- 邻居在网格内部(
- 因为循环只执行 444 次,所以这个减少操作
current_perimeter -= 1
也最多只能被执行 444 次。
极端情况举例
陆地单元格位置 | 邻居 (‘1’) 个数 | current_perimeter 变化 | 最终 current_perimeter | 意义 |
---|---|---|---|---|
孤立点 (周围都是 ‘0’ 或边界) | 0 | 4 - 0 = 4 | 4 | 贡献了 444 条边到周长 |
角点 (被 222 个陆地邻居包围) | 2 | 4 - 2 = 2 | 2 | 贡献了 222 条边到周长 |
边点 (被 333 个陆地邻居包围) | 3 | 4 - 3 = 1 | 1 | 贡献了 111 条边到周长 |
中心点 (被 444 个陆地邻居包围) | 4 | 4 - 4 = 0 | 0 | 贡献了 000 条边到周长 |
因此,这个算法设计的核心正是基于每个方格最多 444 条边的事实,通过 444 次固定的检查来准确计算其净贡献。
🧩 回顾:整体框架
你可以先记住,这段代码有三层逻辑:
1️⃣ 外层两重 for 循环:遍历每个格子
2️⃣ 内层 for 循环:检查当前格子的四个方向(上、下、左、右)
3️⃣ 判断条件:
→ 如果邻居是陆地,就把当前格子的边数减 1
🧱 举个例子(直观)
我们用一个 3×3 的小网格来“跑一遍”:
grid = [[0, 1, 0],[1, 1, 1],[0, 1, 0]
]
岛屿长这样:
010111010
第一步:定义方向
directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
这 4 个方向可以让我们从当前格子 (r, c) 移动到四个相邻格子 (nr, nc)。
第二步:外层循环(从左上角开始扫描)
程序执行顺序:
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
),所以开始执行:
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
📊 第四步:累加结果
每遇到一个陆地格子:
perimeter += current_perimeter
程序会把每个陆地的“贡献周长”都加起来。
最后返回:
return perimeter
总结运行逻辑图(文字版)
for 每一行for 每一列if 当前格是陆地:current_perimeter = 4for 四个方向:if 邻居在范围内 且 是陆地:current_perimeter -= 1总周长 += current_perimeter
return 总周长