图论基础原理与题目说明

图论基础原理与题目说明

文章目录

🔗 查看完整专栏(LeetCode基础算法专栏)

特别说明:

本文为个人的 LeetCode 刷题与学习笔记,内容仅供学习与交流使用,禁止转载或用于商业用途。需要强调的是,文中的题目解法不一定是最优解(可能存在时间或空间复杂度的进一步优化空间),主要目的是分享个人的解题思路与逻辑实现,仅供参考。 笔记内容为个人理解与总结,可能存在疏漏或偏差,欢迎读者自行甄别并交流探讨。

一、 图论基本概念

1.1 图的核心定义

图是由顶点(节点)**和**边 组成的结构(记为 G = ( V , E ) G=(V,E) G=(V,E))。

分类维度 类型 特点(刷题关注点)
边的方向 无向图 边无方向(如社交网络的好友关系)
有向图 边有方向(如道路单行道、任务依赖关系)
边的权重 无权图 边无值(仅表示连通状态)
有权图 边有值(如两点间的距离、费用、时间等)
环的存在 无环图 无闭环(如经典的 DAG:有向无环图)
有环图 存在闭环(遍历时必须加标记以避免死循环)

1.2 图的存储方式

  • 邻接表:用数组或哈希表存储每个节点的相邻节点列表(空间复杂度通常更优,最常用)。
  • 邻接矩阵 :二维数组 matrix[i][j]。值为 1 代表相连,0 代表不连;若为有权图则直接存储权重值。

二、 图的遍历核心:DFS 与 BFS

图的遍历是所有图论算法的基石,本质上与二叉树的遍历同源。唯一且最致命的区别在于:图可能存在环,必须利用 visited 标记访问状态,防止无限死循环。

2.1 DFS(深度优先遍历)

  • 核心思路:从起点出发,沿着一条路径走到黑(不撞南墙不回头),再回溯走其他路径(同二叉树的前序/后序递归逻辑)。
  • 基础模板(邻接表 + 递归):
py 复制代码
def dfs(node, adj, visited):
    visited[node] = True # 标记当前节点已访问
    # print(node)        # 访问节点(执行业务逻辑)
    
    # 遍历所有相邻节点
    for neighbor in adj[node]:
        if not visited[neighbor]:
            dfs(neighbor, adj, visited)

2.2 BFS(广度优先遍历)

  • 核心思路:从起点出发,像水波纹一样,先访问所有直接相邻的节点(一层),再访问相邻节点的相邻节点(下一层)(同二叉树的层序遍历逻辑)。
  • 基础模板(邻接表 + 队列):
py 复制代码
from collections import deque

def bfs(start, adj, visited):
    q = deque([start])
    visited[start] = True
    
    while q:
        node = q.popleft()
        # print(node)    # 访问节点
        
        for neighbor in adj[node]:
            if not visited[neighbor]:
                visited[neighbor] = True
                q.append(neighbor)

三、 高阶概念:有向图与 Trie 树

3.1 入度与出度(拓扑排序前置知识)

在有向图中,入度 与出度是描述「节点与边」关系的核心指标。

概念 通俗定义 数学定义
入度 指向当前节点的边的数量(有多少个 "前驱" 指向我) i n _ d e g r e e ( u ) in\_degree(u) in_degree(u) = 以 u u u 为终点的边数
出度 从当前节点出发的边的数量(我有多少个 "后继") o u t _ d e g r e e ( u ) out\_degree(u) out_degree(u) = 以 u u u 为起点的边数
  • 入度为 0 的节点:没有任何前驱依赖,可以直接作为起点开始。
  • 出度为 0 的节点:没有任何后继依赖,是终点。
  • 拓扑排序 :把有向无环图(DAG)的节点排成一个序列,满足 "所有边的起点必须排在终点的前面"。若图中有环,则无法完成拓扑排序(因为永远找不到入度为 0 的起始节点)。

3.2 Trie(前缀树 / 字典树)

Trie 是专门用于高效存储和检索字符串集合的树形数据结构。

  • 核心优势 :「共享字符串前缀」。例如存储 "app" 和 "apple" 时,前缀 "app" 会被共享,大幅节省空间。检索时间仅与目标字符串长度相关,与集合大小无关。
  • 节点设计 :
    1. children:字典(key=字符,value=子节点),存储当前节点的所有分支。
    2. is_end:布尔值,标记该节点是否是某个完整单词的结尾。

四、 图论算法实战演练

200. 岛屿数量

题目描述:

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

解题思路:

选用DFS(深度优先搜索)嵌套递归法。

遍历网格,遇到未被访问的陆地('1')时,启动一次 DFS 递归。在 DFS 过程中,将该陆地及其所有相连的陆地原地修改为水('0') 。这相当于利用原网格代替了 visited 数组。每启动一次 DFS 就代表消灭并找到了一个独立岛屿,最终统计 DFS 的启动次数即可。

核心代码:

py 复制代码
from typing import List

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        if not grid or not grid[0]:
            return 0
        
        rows = len(grid)
        cols = len(grid[0])
        ans = 0

        # 定义 DFS 函数,作用是淹没 (x,y) 所在的整个相连岛屿
        def dfs(x, y):
            # 递归终止条件:越界,或者当前单元格已经是水
            if not 0 <= x < rows or not 0 <= y < cols or grid[x][y] == '0':
                return
            
            # 核心操作:将当前陆地标记为水(替代 visited 数组)
            grid[x][y] = '0'

            # 递归遍历上下左右,淹没相连陆地
            dfs(x - 1, y)  
            dfs(x + 1, y)  
            dfs(x, y - 1)  
            dfs(x, y + 1)  

        # 遍历网格寻找岛屿起点
        for i in range(rows):
            for j in range(cols):
                if grid[i][j] == '1':
                    ans += 1  # 发现新岛屿
                    dfs(i, j) # 启动 DFS 淹没它
        
        return ans

994. 腐烂的橘子

题目描述:

每分钟,腐烂的橘子周围 4 个方向上相邻的新鲜橘子都会腐烂。返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能全腐烂,返回 -1。

解题思路:

  • 「同时腐烂」说明过程是分层扩散的;

  • 「多个初始腐烂点」说明需从多个起点同时开始。

    这完美契合 多源 BFS 算法:

  1. 初始化:将所有初始腐烂橘子的坐标加入队列,并统计新鲜橘子的总数。
  2. 分层 BFS:每一层循环代表"过了一分钟"。逐个取出队列中的腐烂橘子向四周扩散,将新鲜橘子腐烂并加入下一层队列,同时新鲜橘子总数减一。
  3. 结果判定 :若最终新鲜橘子数 > 0 >0 >0,返回 -1;否则返回统计的分钟数。

核心代码:

py 复制代码
import collections
from typing import List

class Solution:
    def orangesRotting(self, grid: List[List[int]]) -> int:
        if not grid or not grid[0]:
            return 0
        
        rows = len(grid)
        cols = len(grid[0])

        queue = collections.deque()
        fresh_num = 0
        time = 0

        # 1. 收集初始腐烂橘子(多源起点)并统计新鲜橘子
        for i in range(rows):
            for j in range(cols):
                if grid[i][j] == 1:
                    fresh_num += 1
                elif grid[i][j] == 2:
                    queue.append((i, j))
        
        dirs = [(-1, 0), (1, 0), (0, -1), (0, 1)]

        # 2. 分层 BFS 模拟腐烂过程
        while queue and fresh_num > 0:
            level_size = len(queue)
            for _ in range(level_size):
                x, y = queue.popleft()
                for dx, dy in dirs:
                    nx, ny = x + dx, y + dy
                    # 发现相邻的新鲜橘子
                    if 0 <= nx < rows and 0 <= ny < cols and grid[nx][ny] == 1:
                        grid[nx][ny] = 2    
                        fresh_num -= 1
                        queue.append((nx, ny))
            time += 1

        return time if fresh_num == 0 else -1

207. 课程表

题目描述:

判断是否可能完成所有课程的学习,课程之间存在先修依赖关系。本质是判断有向图中是否存在环。

解题思路:

使用基于入度 的 BFS 拓扑排序:

  1. 构建图 :构建邻接表 adj 记录每门课的后续课程,构建 in_degree 数组记录每门课的前置条件数量。
  2. 初始化队列:将所有入度为 0(无前置依赖)的课程加入队列。
  3. BFS 模拟修课:取出队列中的课(算作修完一门)。然后将其所有后续课程的入度减 1。若后续课程的入度降为 0,说明其前置条件已全部满足,加入队列。
  4. 结果判定:最后统计修完的课程数,若等于总课数则说明无环(可以完成选课)。

核心代码:

py 复制代码
import collections
from typing import List

class Solution:
    def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
        # 1. 初始化邻接表和入度数组
        adj = [[] for _ in range(numCourses)]
        in_degree = [0] * numCourses

        for a, b in prerequisites:  
            adj[b].append(a)       # b -> a:修 a 需先修 b
            in_degree[a] += 1
        
        # 2. 收集所有入度为 0 的节点(可以直接学的课)
        queue = collections.deque()
        for i in range(numCourses):
            if in_degree[i] == 0:   
                queue.append(i)
        
        # 3. BFS 模拟选课过程
        processed = 0 
        while queue:
            course = queue.popleft()
            processed += 1

            for next_course in adj[course]:
                in_degree[next_course] -= 1
                # 前置条件全部满足,可以开始学了
                if in_degree[next_course] == 0:
                    queue.append(next_course)
            
        return processed == numCourses

208. 实现 Trie (前缀树)

解题思路:

根据前述理论,构建 TrieNode 辅助类,随后实现树的初始化、插入和查询逻辑。对于 startsWith,只需路径存在即可返回 True,无需校验 is_end。

核心代码:

py 复制代码
class TrieNode:
    """定义 Trie 的节点类"""
    def __init__(self):
        self.children = {}   # key: 字符, value: 对应的 TrieNode
        self.is_end = False  # 标记是否是单词结尾

class Trie:
    def __init__(self):
        self.root = TrieNode()  # 根节点不存储实际字符

    def insert(self, word: str) -> None:
        current_node = self.root
        for char in word:
            # 字符不存在对应分支则创建
            if char not in current_node.children:
                current_node.children[char] = TrieNode()
            current_node = current_node.children[char]
        # 单词插入完毕,打上结尾标记
        current_node.is_end = True

    def search(self, word: str) -> bool:
        current_node = self.root
        for char in word:
            if char not in current_node.children:
                return False
            current_node = current_node.children[char]
        # 必须是某个完整单词的结尾
        return current_node.is_end

    def startsWith(self, prefix: str) -> bool:
        current_node = self.root
        for char in prefix:
            if char not in current_node.children:
                return False
            current_node = current_node.children[char]
        # 只要存在这条路径即可,无需判断 is_end
        return True
相关推荐
sell-cloud3 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国4 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI4 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区5 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris6 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy7 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V7 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI7 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技7 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人
gCode Teacher 格码致知7 小时前
Pandas教学-17:Series与Dataframe的关系
开发语言·python·pandas