Leetcode 图论 1

题号直达: 200 岛屿数量 | 994 腐烂的橘子 | 207 课程表 | 208 实现 Trie

LeetCode 200. 岛屿数量 ↑

题目

给你一个由 '1'(陆地)和 '0'(水)组成的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,且只通过水平方向或垂直方向上相邻的陆地连接。你可以假设该网格的四条边均被水包围。

图论视角: 将每个 '1' 视为顶点,四邻接的陆地连边,则岛屿数 = 连通分量个数;本题在网格上用 DFS/BFS 遍历即可。

提示

  • m == len(grid),n == len(grid[i])
  • 1 ≤ m, n ≤ 300
  • grid[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 ≤ 10
  • grid[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 ≤ 2000
  • 0 ≤ len(prerequisites) ≤ 5000,且 len(prerequisites[i]) == 2
  • 0 ≤ 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) ≤ 2000
  • word、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(总字符数)。
posted @ 2026-10-12 00:13  Phier  阅读(3)  评论(0)    收藏  举报