9.1 LeetCode 热题 100 (图论)

岛屿数量

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

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

 输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
 ]
 输出:1

示例 2:

 输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
 ]
 输出:3

思路

用 DFS 去“淹掉”一个岛屿的所有陆地块,每当遇到一个新的“1”,就调用一次 DFS,把相邻陆地设为“0”,并且岛屿数加一。

Code

DFS:

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        m, n = len(grid), len(grid[0])
        count = 0
        def dfs(a,b):
            if grid[a][b] != "1":
                return
            grid[a][b] = "0"
            if b>0:
                dfs(a, b-1)
            if n-b > 1:
                dfs(a, b+1)
            if m-a > 1:
                dfs(a+1, b)
            if a > 0:
                dfs(a-1, b)

        for a in range(m):
            for b in range(n):
                if grid[a][b] == "1":
                    dfs(a,b)
                    count+=1
        return count
Python





腐烂的橘子

在给定的 m x n 网格 grid 中,每个单元格可以有以下三个值之一:

  • 0 代表空单元格;
  • 1 代表新鲜橘子;
  • 2 代表腐烂的橘子。

每分钟,腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂。

返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1

示例 1:

img
 输入:grid = [[2,1,1],[1,1,0],[0,1,1]]
 输出:4

示例 2:

 输入:grid = [[2,1,1],[0,1,1],[1,0,1]]
 输出:-1
 解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个方向上。

示例 3:

 输入:grid = [[0,2]]
 输出:0
 解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。

想法梳理 + 优化

预处理阶段:

  • 遍历整个网格,统计新鲜橘子的数量 fresh_count
  • 同时将所有腐烂橘子的位置加入队列 queue,作为 BFS 的起点。

👉 这里不需要一开始就判断某个新鲜橘子是否完全被隔离,因为 BFS 会自然处理到这一点。


BFS 腐烂传播过程:

  • 使用 BFS,每轮代表“1分钟”,遍历队列中当前腐烂的橘子。
  • 对它的上下左右 4 个方向的格子:
    • 如果是新鲜橘子(值为 1),将其改为腐烂(设为 2),并加入队列。
    • 同时 fresh_count -= 1
  • 每完成一轮(即当前层所有腐烂橘子处理完),时间 +1。

终止条件:

  • 当队列为空时,检查 fresh_count
    • 如果 fresh_count == 0,返回时间。
    • 否则,返回 -1(说明有新鲜橘子无法被腐烂)。

小细节提醒:

  • 可以用队列 collections.deque 实现高效的 BFS。
  • 每一轮 BFS 都要记录当前队列大小(即当前层腐烂橘子的数量),作为本轮腐烂传播的“起点数”。
  • 时间计数可以在每一轮后 time += 1,但要注意如果一开始就没有新鲜橘子应该直接返回 0。

Code:

from collections import deque

class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        m, n = len(grid), len(grid[0])
        fresh_count = 0
        queue = deque()
        for i in range(m):
            for j in range(n):
                if grid[i][j] == 1:
                    fresh_count += 1
                if grid[i][j] == 2:
                    queue.append((i,j))
        directions = [(1,0), (-1,0), (0,1), (0,-1)]
        minute = 0
        while queue and fresh_count > 0:
            for _ in range(len(queue)):
                x, y = queue.popleft()
                for dx, dy in directions:
                    nx, ny = (x + dx), (y + dy)
                    if 0 <= nx < m and 0 <= ny < n and grid[nx][ny]==1:
                        grid[nx][ny] = 2
                        fresh_count -= 1
                        queue.append((nx,ny))
            minute += 1
        return minute if fresh_count == 0 else -1
        
Python




时间和空间复杂度分析

  • 时间复杂度:O(m × n),每个格子最多只访问一次。
  • 空间复杂度:O(m × n),最坏情况下所有橘子都进队列。

课程表

你这个学期必须选修 numCourses 门课程,记为 0numCourses - 1

在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai必须 先学习课程 bi

  • 例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1

请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false

示例 1:

 输入:numCourses = 2, prerequisites = [[1,0]]
 输出:true
 解释:总共有 2 门课程。学习课程 1 之前,你需要完成课程 0 。这是可能的。

示例 2:

 输入:numCourses = 2, prerequisites = [[1,0],[0,1]]
 输出:false
 解释:总共有 2 门课程。学习课程 1 之前,你需要先完成课程 0 ;并且学习课程 0 之前,你还应先完成课程 1 。这是不可能的。

这道题的本质是一个图的拓扑排序问题,可以抽象成:

✅ 问题建模

  • 每门课程是图中的一个节点。
  • 每个先修关系 [a, b] 表示一条边:b → a
  • 如果这个有向图中存在 ,说明有课程循环依赖,无法完成全部课程
  • 如果图是 无环有向图(DAG),说明课程是有序的,可以按某种顺序完成全部课程。

✅ 检测有向图是否有环的常见方法:

1. Kahn’s 算法(BFS 拓扑排序):

  • 统计每个节点的入度;
  • 每次找入度为 0 的节点,移除并更新它指向的节点的入度;
  • 如果最终处理的节点数量 < 总课程数,说明存在环。

2. DFS + 访问状态标记:

  • 对每个节点进行 DFS,使用状态数组记录访问状态:
    • 0:未访问
    • 1:访问中(当前 DFS 路径中)
    • 2:访问完成
  • 如果在 DFS 中再次遇到访问中的节点(状态为 1),说明出现环。

Code: DFS

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        visited = [0] * numCourses
        pre = defaultdict(list)
        for a, b in prerequisites:
            pre[b].append(a)

        def dfs(course):
            if visited[course] == 1:
                return False
            elif visited[course] == 2:
                return True
            visited[course] = 1
            for neighbor in pre[course]:
                if not dfs(neighbor):
                    return False
            else:
                visited[course] = 2
                return True
        
        for course in range(numCourses):
            if not dfs(course):
                return False
        return True
Python




graph[b].append(a) 表示 b 是 a 的前置课。

DFS 中状态判断逻辑:

  • 1 是“当前路径正在访问”,用于检测环。
  • 2 是“访问完成”,可直接跳过。

DFS 过程中一旦发现环,立即返回 False。

对所有课程都进行 DFS 避免遗漏不连通图部分。

复杂度分析:

  • 时间复杂度:O(V + E),其中 V 是课程数,E 是先修边数。
  • 空间复杂度:O(V + E),用于图的邻接表和递归栈。

实现 Trie (前缀树)

Trie(发音类似 “try”)或者说 前缀树 是一种树形数据结构,用于高效地存储和检索字符串数据集中的键。这一数据结构有相当多的应用情景,例如自动补全和拼写检查。

请你实现 Trie 类:

  • Trie() 初始化前缀树对象。
  • void insert(String word) 向前缀树中插入字符串 word
  • boolean search(String word) 如果字符串 word 在前缀树中,返回 true(即,在检索之前已经插入);否则,返回 false
  • boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ,返回 true ;否则,返回 false

示例:

 输入
 ["Trie", "insert", "search", "search", "startsWith", "insert", "search"]
 [[], ["apple"], ["apple"], ["app"], ["app"], ["app"], ["app"]]
 输出
 [null, null, true, false, true, null, true]
 ​
 解释
 Trie trie = new Trie();
 trie.insert("apple");
 trie.search("apple");   // 返回 True
 trie.search("app");     // 返回 False
 trie.startsWith("app"); // 返回 True
 trie.insert("app");
 trie.search("app");     // 返回 True

Trie 树结构思路整理

基本结构设计

  • 每个节点用 Node 类表示,包含两个成员:
    • son: 一个字典,key 是字符,value 是子节点。
    • end: 一个布尔值,表示是否是某个单词的结尾。

Trie 类设计

  • 使用一个 root 节点作为前缀树的起点。
  • 提供三个核心方法:insert, search, startsWith

方法功能与思路

  1. insert(word)
  • root 开始,逐字符遍历 word
  • 如果当前字符在当前节点的 son 中不存在,创建新的子节点。
  • 最后将最后一个字符节点的 end 标记为 True
  1. search(word)
  • 同样从 root 开始,逐字符查找。
  • 如果路径中有字符不存在于 son 中,直接返回 False
  • 如果路径存在,则返回最后一个节点的 end 值,判断是否为完整单词。
  1. startsWith(prefix)
  • 类似于 search,但不关心最后一个字符是否为单词结束。
  • 只要路径完整存在,说明前缀存在。

Code:

class Node:
    def __init__(self):
        self.son = {}
        self.end = False

class Trie:
    def __init__(self):
        self.root = Node()

    def insert(self, word: str) -> None:
        cur = self.root
        for s in word:
            if s not in cur.son:
                cur.son[s] = Node()
            cur = cur.son[s]
        cur.end = True

    def search(self, word: str) -> bool:
        cur = self.root
        for s in word:
            if s not in cur.son:
                return False
            cur = cur.son[s]
        return True if cur.end else False

    def startsWith(self, prefix: str) -> bool:
        cur = self.root
        for s in prefix:
            if s not in cur.son:
                return False
            cur = cur.son[s]
        return True
Python




时间与空间复杂度

  • 时间复杂度
    • insert, search: O(n),n 是字符串长度。
    • startsWith: O(m),m 是前缀长度。
  • 空间复杂度
    • 最坏情况下插入 N 个长度为 L 的互不相同单词,需要 O(N×L) 空间。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇