图论基础原理与题目说明

图论基础原理与题目说明

文章目录

🔗 查看完整专栏(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
相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第53题】【JVM篇】第13题:JVM采用什么算法判断一个对象是否需要被回收?
java·jvm·算法·面试
code bean1 小时前
【Langchain】 ChatPromptTemplate:从“手动拼字符串“到“专业模板“的进化之路
人工智能·机器学习·langchain
2301_781571421 小时前
Go语言如何用sqlx_Go语言sqlx数据库操作教程【入门】
jvm·数据库·python
Aipollo1 小时前
AI助手模块工作流程技术总结
人工智能·ai
2401_880071401 小时前
mysql安装后如何进行初始化安全配置_mysql_secure_installation实操
jvm·数据库·python
eastyuxiao1 小时前
主流物联网协议 超详细讲解
大数据·人工智能·物联网·智慧城市·能源·数字孪生
z200509301 小时前
今日算法(二叉树)
数据结构
小赵不会秃头1 小时前
数据结构Day 06:线性结构、库操作及 Makefile 完整学习笔记
java·linux·数据结构·算法·面试
m0_609160491 小时前
如何创建物化视图日志_CREATE MATERIALIZED VIEW LOG记录基表DML变更.txt
jvm·数据库·python