Leetcode 图论 1
题号直达: 200 岛屿数量 | 994 腐烂的橘子 | 207 课程表 | 208 实现 Trie
LeetCode 200. 岛屿数量 ↑
题目
给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,且只通过水平方向或垂直方向上相邻的陆地连接。你可以假设该网格的四条边均被水包围。
图论视角: 将每个 '1' 视为顶点,四邻接的陆地连边,则岛屿数 = 连通分量个数;本题在网格上用 DFS/BFS 遍历即可。
提示
m == len(grid),n == len(grid[i])1 ≤ m, n ≤ 300grid[i][j]为字符'0'或'1'
代码(Python)解法一:DFS 沉没岛屿
每遇到一块 '1',岛屿计数 +1,并从该格出发 DFS,把与该岛相连的所有 '1' 改为 '0'(标记已访问),避免重复计数。
class Solution(object):
def dfs(self, grid, r, c):
nr = len(grid)
nc = len(grid[0])
grid[r][c] = "0"
if r - 1 >= 0 and grid[r - 1][c] == "1":
self.dfs(grid, r - 1, c)
if r + 1 < nr and grid[r + 1][c] == "1":
self.dfs(grid, r + 1, c)
if c - 1 >= 0 and grid[r][c - 1] == "1":
self.dfs(grid, r, c - 1)
if c + 1 < nc and grid[r][c + 1] == "1":
self.dfs(grid, r, c + 1)
def numIslands(self, grid):
"""
:type grid: List[List[str]]
:rtype: int
"""
nr = len(grid)
if nr == 0:
return 0
nc = len(grid[0])
num_islands = 0
for r in range(nr):
for c in range(nc):
if grid[r][c] == "1":
num_islands += 1
self.dfs(grid, r, c)
return num_islands
解法一 讲解视频(讲解视频文件 0403_1.mp4)
思路解析(解法一)
- 连通块:每个四连通的
'1'块即一座岛;枚举每个未访问的'1'作为新岛起点。 - 沉没:
dfs先将当前格置'0',再向上下左右递归,整块岛被抹成水,后续二重循环不会再碰到。 - 边界:行
0 … nr-1、列0 … nc-1,越界不递归。 - 空网格:
nr == 0时返回 0。 - 复杂度:每个格子最多被访问常数次,时间 O(mn),递归栈最坏 O(mn)(全为
'1'时深度与形状有关;亦可改用 BFS,见解法二)。
代码(Python)解法二:BFS(队列 deque)
每遇到一块 '1',岛屿计数 +1,将该格入队并 BFS:弹出队列中的格子,把四邻仍为 '1' 的格子入队并置 '0',直到队列为空。
from collections import deque
class Solution(object):
def numIslands(self, grid):
"""
:type grid: List[List[str]]
:rtype: int
"""
if not grid:
return 0
nr = len(grid)
nc = len(grid[0])
num_islands = 0
for r in range(nr):
for c in range(nc):
if grid[r][c] == "1":
num_islands += 1
grid[r][c] = "0"
neighbors = deque()
neighbors.append((r, c))
while neighbors:
row, col = neighbors.popleft()
if row - 1 >= 0 and grid[row - 1][col] == "1":
neighbors.append((row - 1, col))
grid[row - 1][col] = "0"
if row + 1 < nr and grid[row + 1][col] == "1":
neighbors.append((row + 1, col))
grid[row + 1][col] = "0"
if col - 1 >= 0 and grid[row][col - 1] == "1":
neighbors.append((row, col - 1))
grid[row][col - 1] = "0"
if col + 1 < nc and grid[row][col + 1] == "1":
neighbors.append((row, col + 1))
grid[row][col + 1] = "0"
return num_islands
思路解析(解法二)
- 队列:
deque左端弹出、右端入队,实现 FIFO,与「先发现的陆地先扩展」一致。 - 入队与染色:发现邻格为
'1'时立刻置'0'并入队,避免同一格被重复入队。 - 起点:与 DFS 一样,外层
if grid[r][c] == "1"时先对起点染色再入队,保证该岛只被统计一次。 - 复杂度:每个格子最多入队、出队各一次,时间 O(mn);队列中同时存在的格子数最坏 O(mn)(例如某一「层」同时扩展很多格),无递归栈,适合极深形状时避免栈溢出(常数与 DFS 不同,二者均可)。
LeetCode 994. 腐烂的橘子 ↑
题目
在给定的 m × n 网格 grid 中,每个格子可以是:
0:空单元格1:新鲜橘子2:腐烂橘子
每分钟,腐烂橘子会使其 上下左右 四邻的新鲜橘子变腐烂。返回直到网格中 没有新鲜橘子 所需的 最少分钟数;若无法使全部新鲜橘子腐烂,返回 -1。
图论视角: 多源 BFS,按「分钟」分层扩散,层数即时间。
提示
m == len(grid),n == len(grid[i])1 ≤ m, n ≤ 10grid[i][j]仅为0、1或2
代码(Python):多源 BFS(按层扩散)
先将所有腐烂格入队,并统计新鲜橘子个数 cnt。若 cnt == 0 直接返回 0。每一轮处理当前队列长度 N,相当于 1 分钟 内同时从所有腐烂格扩展;新鲜变腐烂时入队并 cnt -= 1。队列为空后:若仍有新鲜则返回 -1,否则返回 time - 1(最后一轮队列可能只做「收尾」而未再感染,需减 1)。
from collections import deque
class Solution(object):
def orangesRotting(self, grid):
"""
:type grid: List[List[int]]
:rtype: int
"""
if not grid:
return -1
nr = len(grid)
nc = len(grid[0])
que = deque()
cnt = 0
for i in range(nr):
for j in range(nc):
if grid[i][j] == 2:
que.append([i, j])
if grid[i][j] == 1:
cnt += 1
if cnt == 0:
return 0
time = 0
while que:
N = len(que)
for i in range(N):
temp = que.popleft()
if temp[0] - 1 >= 0:
if grid[temp[0] - 1][temp[1]] == 1:
grid[temp[0] - 1][temp[1]] = 2
que.append([temp[0] - 1, temp[1]])
cnt -= 1
if temp[0] + 1 <= len(grid) - 1:
if grid[temp[0] + 1][temp[1]] == 1:
grid[temp[0] + 1][temp[1]] = 2
que.append([temp[0] + 1, temp[1]])
cnt -= 1
if temp[1] - 1 >= 0:
if grid[temp[0]][temp[1] - 1] == 1:
grid[temp[0]][temp[1] - 1] = 2
que.append([temp[0], temp[1] - 1])
cnt -= 1
if temp[1] + 1 <= len(grid[0]) - 1:
if grid[temp[0]][temp[1] + 1] == 1:
grid[temp[0]][temp[1] + 1] = 2
que.append([temp[0], temp[1] + 1])
cnt -= 1
time += 1
if cnt > 0:
return -1
else:
return time - 1
思路解析
- 多源 BFS:所有
2同时作为起点;每层for i in range(N)对应 1 分钟 的并行扩散。 cnt:统计新鲜橘子;每次感染则减 1,避免最后再扫一遍网格。- 返回
time - 1:循环每轮先扩展再time += 1,队列为空时多计了最后一轮「已无新鲜可感染」的层,故分钟数为time - 1。(也可用「先time += 1再判队列空」等写法避免减 1,本质一致。) - 复杂度:每个格子最多入队、出队一次,时间 O(mn),队列 O(mn)。
LeetCode 207. 课程表 ↑
题目
你总共需要修 numCourses 门课,编号为 0 到 numCourses - 1。部分课程有先修要求,以二维数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] 表示:要学习课程 ai,必须先完成课程 bi(例如 [0, 1] 表示学课程 0 前须先学完课程 1)。
判断能否修完所有课程:若存在合法上课顺序则返回 true;若先修关系形成环(互相依赖)则无法修完,返回 false。
图论视角: 课程为有向边 bi → ai(先修指向后续),能否修完 = 有向图是否存在拓扑排序(无环则存在)。
提示
1 ≤ numCourses ≤ 20000 ≤ len(prerequisites) ≤ 5000,且len(prerequisites[i]) == 20 ≤ ai, bi < numCourses,且各先修对 互不相同
代码(Python):Kahn 拓扑排序(BFS 入度)
用邻接表 edges 存「先修课 → 后继课」列表,数组 indeg 记录每门课的入度(还剩多少先修未完成)。将所有入度为 0 的课入队,反复弹出、选修该课并令后继入度减 1;若某后继入度变为 0 则入队。若最终选修次数等于 numCourses,说明无环且存在拓扑序。
from collections import defaultdict, deque
class Solution(object):
def canFinish(self, numCourses, prerequisites):
"""
:type numCourses: int
:type prerequisites: List[List[int]]
:rtype: bool
"""
edges = defaultdict(list)
indeg = [0] * numCourses
for info in prerequisites:
edges[info[1]].append(info[0])
indeg[info[0]] += 1
q = deque([u for u in range(numCourses) if indeg[u] == 0])
visited = 0
while q:
visited += 1
u = q.popleft()
for v in edges[u]:
indeg[v] -= 1
if indeg[v] == 0:
q.append(v)
return visited == numCourses
思路解析
- 建图:
edges[x]列出「以 x 为先修」的后继课程;对每条边[a, b]含义为修a前须先修b,即b → a,故edges[b].append(a)且indeg[a] += 1。 - Kahn:不断删除入度为 0 的结点(可视为已修完),并更新后继入度;若图中无环,最终能删完
numCourses个结点。 - 判定:
visited == numCourses等价于「拓扑排序成功」;若存在环,环上结点入度无法全部减到 0,visited将小于numCourses。 - 复杂度:建图与遍历边各一次,时间 O(V + E),其中
V = numCourses,E = len(prerequisites);空间 O(V + E)。
LeetCode 208. 实现 Trie(前缀树)↑
题目
Trie(前缀树) 是一种树形结构,用于高效存储与检索字符串集合,常用于自动补全、拼写检查等。
实现 Trie 类:
Trie():初始化前缀树。insert(word):插入字符串word。search(word):若word曾被完整插入则返回true,否则false。startsWith(prefix):若存在已插入串以prefix为前缀则返回true,否则false。
示例要点: 先 insert("apple") 后,search("apple") 为 true,search("app") 为 false(未作为整词插入),startsWith("app") 为 true;再 insert("app") 后,search("app") 为 true。
提示
1 ≤ len(word), len(prefix) ≤ 2000word、prefix仅含英文小写字母insert、search、startsWith总调用次数不超过3 × 10^4
代码(Python):字典树结点 + 子结点哈希表
根结点不表示字符;从根向下每条边用字符作 key 指向子结点。insert 沿路径建边,在最后一个字符结点置 is_end = True 表示此处结束了一个完整单词。search 除路径存在外还须 is_end;startsWith 只需沿前缀能走通即可。
子结点用 dict 存储,新路径按需创建 Treenode。注意: 结点 __init__ 中不要用 children=dict() 作为默认参数(可变对象会被所有实例共享),应使用 children=None 再在函数体内赋空字典。
class Treenode(object):
def __init__(self, val=None, children=None):
self.val = val
self.children = children if children is not None else {}
self.is_end = False
class Trie(object):
def __init__(self):
self.root = Treenode(None, {})
def insert(self, word):
"""
:type word: str
:rtype: None
"""
node = self.root
for x in word:
if node.children.get(x, None):
node = node.children[x]
else:
newnode = Treenode(x, {})
node.children[x] = newnode
node = newnode
node.is_end = True
def search(self, word):
"""
:type word: str
:rtype: bool
"""
node = self.root
for x in word:
if node.children.get(x, None):
node = node.children[x]
else:
return False
if node.is_end:
return True
return False
def startsWith(self, prefix):
"""
:type prefix: str
:rtype: bool
"""
node = self.root
for x in prefix:
if node.children.get(x, None):
node = node.children[x]
else:
return False
return True
思路解析
- 前缀共享:多条单词共用相同前缀路径,节省空间;插入
"app"再插入"apple"时,"app"路径上最后一个结点单独置is_end,继续延伸"le"。 is_end:区分「路径存在」与「此处是某次insert的结尾」;故先只插入"apple"时,search("app")为false,startsWith("app")为true。- 复杂度:单次操作沿串长度走一遍,时间 O(L)(
L为word或prefix长度);空间为所有插入字符总数 O(总字符数)。

浙公网安备 33010602011771号