Grid as Graph網格圖
把二維陣列當圖,四方向就是邊。
用在:數島嶼、迷宮、影像連通區域
01為什麼需要它
一張切片掃描成 4000×3000 像素,二值化後細胞核是 1、背景是 0。醫檢系統要數出有幾個細胞核,並標出面積超過 500 像素、可能是病變的大塊。
為什麼用它每個像素是節點,上下左右都是 1 就有邊,一個細胞核就是一個連通分量。逐格掃描,碰到沒走過的 1 就把整塊走完、順便累加面積。1200 萬個像素每個只進出佇列一次,而且鄰居用座標算出來,不必真的建一張 1200 萬節點的鄰接串列。
一份 2000×2000 格的地形高程圖,每格記錄海拔。低於 2 公尺的格子有幾十萬個,但其中有些窪地被堤防和高地圍住,海水根本進不去。
為什麼用它只看海拔會把內陸窪地也算成淹水。反過來從海出發:把所有海洋格同時放進佇列,只往海拔 ≤ 2 公尺的鄰格擴散,走得到的才會淹。一次走訪 O(mn),不必對每個低窪格各自檢查「通不通海」。
一層 200×300 格的賣場平面圖,貨架是牆,共有 6 個逃生出口。消防規定每一區的指示牌要標出走到最近出口的步數。
為什麼用它對 6 個出口各跑一次 BFS 再取最小值,要走 6 趟。多源 BFS 把 6 個出口一開始全放進佇列、距離都是 0,一趟走訪就得到每格到「最近」出口的步數,等同於加一個連到所有出口的虛擬起點。
看到這些關鍵字就想到它:二維網格、m × n、上下左右相鄰、有幾塊/幾座島、最大的一塊、填色、迷宮最少步數、到最近的某物的距離、從邊界往內走。
02核心概念
網格本身就是一張圖:每一格 (r, c) 是節點,上下左右相鄰、而且兩格都可以走時,中間就有一條無向邊。這些邊不需要存下來:用方向陣列 DIRS = [(-1,0), (1,0), (0,-1), (0,1)] 加上座標就能即時算出鄰居,所以 BFS 與 DFS 可以原封不動搬過來,唯一的差別是「列出 u 的鄰居」從查鄰接串列換成四次邊界檢查。一個 m × n 的網格有 V = mn 個節點;全部格子都能走時邊最多,E = m(n−1) + n(m−1) < 2mn。
網格題大多是兩種問法。數連通區域:雙重迴圈逐格掃描,碰到還沒拜訪的可走格就 count += 1,從它出發把整塊走完並標記。這樣數出來恰好是分量個數,因為一次走訪會標記起點所在分量的每一格(相連的格子一定會被放入),也只標記那個分量(不相連的格子沒有邊可以到達);之後掃描再碰到未拜訪的格子,它必定屬於一個還沒數過的分量。最少步數:每走一步代價都是 1,所以用 BFS,一格第一次被標記時的 dist 就是最短步數。若起點有很多個,就把它們同時以距離 0 放進佇列,這叫多源 BFS,得到的是每格到「最近」起點的距離;問「哪些格子碰不到邊界」時,也是把邊界上所有可走的格子當起點反向走一次,沒被走到的就是答案。
複雜度:每格最多被標記一次、進出佇列一次,每次檢查 4 個方向,時間 O(4mn) = O(mn),就是 O(V + E) 代入網格的結果。外層掃描本身就要看過每一格,所以數區域沒有更好的最佳情況。空間是 visited 或 dist 陣列的 O(mn),佇列或堆疊最壞也可能同時放著 O(mn) 格。DFS 用遞迴寫時,遞迴深度等於目前路徑長,一條蛇形的陸地就能讓它深到 mn 層:1000×1000 的網格是一百萬層,Python 預設遞迴上限只有 1000,C++ 的預設堆疊通常也撐不住,大網格請用 BFS 或明確的堆疊。
常見的錯:先檢查邊界再取值,Python 的 grid[-1] 不會報錯,而是取到最後一列,越界的 -1 會悄悄接到對面;放入佇列時就標記,等到取出才標記的話,同一格會被好幾個鄰居重複放入;四方向還是八方向要看題目,斜走也算一步時 DIRS 要有 8 個;直接把走過的陸地改成 0 可以省下 visited,但會破壞輸入。和 Word Search 的網格回溯不同,這裡標記之後永遠不取消,每格只走一次才有 O(mn)。格子各有不同的通過代價時改用 Dijkstra;格子會一個一個變成陸地、還要隨時回答島嶼數時,改用併查集。
03演算法步驟
- 1定義圖:
m = len(grid)、n = len(grid[0]),決定哪些格子可以走(陸地、非牆),寫好DIRS(四方向或八方向)。不要另外建鄰接串列。 - 2準備標記:
m × n的seen布林陣列;需要步數就用dist陣列,-1代表還沒走到。 - 3走訪一塊:起點標記後放入佇列。取出
(r, c),對每個(dr, dc)算出(nr, nc),依序檢查0 ≤ nr < m、0 ≤ nc < n、可以走、還沒標記,全部通過才標記並放入。 - 4數區域:雙重迴圈掃每一格,碰到還沒標記的可走格就把區域數加一,從它開始執行步驟 3,要面積就在取出時累加。
- 5最少步數:單一起點直接 BFS,
dist[nr][nc] = dist[r][c] + 1;多個起點(最近的出口、所有海洋格、所有邊界格)先全部以距離 0 放入佇列,再跑同一個迴圈。
04互動示範
5×6 的地圖,1 是陸地、0 是水,共有 5 座島。示範逐格掃描,碰到沒走過的陸地就把整座島走完:黃色是在佇列或堆疊裡等待的格子,藍色是正在處理的格子,綠框是這一步找到、可以走的鄰居,被取出處理的格子會換成它所屬島的編號。切換「BFS(佇列)」與「DFS(堆疊)」,程式只差在從前端還是尾端取出:同一座島裡格子完成的順序不同,但最後島嶼數與每座島的格子完全一樣。
05程式碼
兩個函式對應兩種問法:island_areas 用 BFS 數出每座島的面積(用的就是示範的地圖),nearest_exit 用多源 BFS 算每格到最近出口的步數,包含一格被牆圍住、走不到的情況。兩者共用同一段「方向陣列 + 邊界檢查 + 放入時標記」的骨架,這段寫熟了,大部分網格題只剩下「哪些格子可以走、起點是誰」要決定。
from collections import deque
DIRS = [(-1, 0), (1, 0), (0, -1), (0, 1)] # 上、下、左、右
def island_areas(grid):
"""四方向相連的 1 是一座島。回傳每座島的面積(依掃描順序),長度就是島嶼數。"""
m, n = len(grid), len(grid[0])
seen = [[False] * n for _ in range(m)]
areas = []
for r in range(m):
for c in range(n):
if grid[r][c] != 1 or seen[r][c]:
continue
seen[r][c] = True # 掃到沒走過的陸地:新島的起點
queue, area = deque([(r, c)]), 0
while queue:
cr, cc = queue.popleft() # 改成 queue.pop() 就是堆疊版,面積不變
area += 1
for dr, dc in DIRS:
nr, nc = cr + dr, cc + dc
# 先檢查邊界:Python 的 grid[-1] 不會報錯,會悄悄取到最後一列
if 0 <= nr < m and 0 <= nc < n and grid[nr][nc] == 1 and not seen[nr][nc]:
seen[nr][nc] = True # 放入時就標記,同一格才不會被放入兩次
queue.append((nr, nc))
areas.append(area)
return areas
def nearest_exit(floor):
"""多源 BFS:每格走到最近的出口 E 要幾步。牆 # 與走不到的格子是 -1。"""
m, n = len(floor), len(floor[0])
dist = [[-1] * n for _ in range(m)]
queue = deque()
for r in range(m):
for c in range(n):
if floor[r][c] == "E":
dist[r][c] = 0 # 所有出口一開始就在佇列裡,距離 0
queue.append((r, c))
while queue:
r, c = queue.popleft()
for dr, dc in DIRS:
nr, nc = r + dr, c + dc
if 0 <= nr < m and 0 <= nc < n and floor[nr][nc] != "#" and dist[nr][nc] == -1:
dist[nr][nc] = dist[r][c] + 1 # 第一次被走到就是最短步數
queue.append((nr, nc))
return dist
if __name__ == "__main__":
grid = [
[1, 1, 0, 0, 0, 1],
[1, 0, 0, 1, 0, 1],
[0, 0, 1, 1, 0, 0],
[0, 0, 0, 1, 0, 0],
[1, 0, 0, 0, 0, 1],
]
areas = island_areas(grid)
print(len(areas), areas, max(areas)) # 5 [3, 2, 4, 1, 1] 4
floor = ["E.#...",
"..#.#.",
"....#E",
"##.##.",
"#.#..."]
for r, row in enumerate(nearest_exit(floor)):
print(" ".join(" #" if floor[r][c] == "#" else f"{d:2}" for c, d in enumerate(row)))
# 輸出(# 是牆;(4, 1) 被牆圍住,走不到任何出口,所以是 -1):
# 0 1 # 4 3 2
# 1 2 # 5 # 1
# 2 3 4 5 # 0
# # # 5 # # 1
# # -1 # 4 3 206練習題
- LeetCode 733Flood Fill(最基本的四方向走訪)Easy
- LeetCode 1020Number of Enclaves(從邊界反向走訪)Medium
- LeetCode 54201 Matrix(多源 BFS)Medium
- LeetCode 417Pacific Atlantic Water Flow(從兩個海岸各走一次)Medium
- LeetCode 934Shortest Bridge(先標出一座島,再多源 BFS)Medium
- LeetCode 827Making A Large Island(先標號記面積,再試每個 0)Hard