【计算机算法与设计(1)】算法课程要点总览:快速回顾与知识地图

AgenticCoding·十二月创作之星挑战赛 10w+人浏览 260人参与

文章目录

📌 适合对象:算法学习者、计算机科学学生、需要快速复习的读者
⏱️ 预计阅读时间:30-40分钟
🎯 学习目标:快速回顾算法课程的核心要点,掌握各算法和知识点的关键特征,建立完整的算法知识体系


📚 学习路线图

算法基础
渐进记号
O、Ω、Θ
递推关系
主方法
算法分析基础
排序算法
比较排序与非比较排序
算法设计方法
分治、动态规划、贪心
图算法基础
BFS、DFS
最短路径
Dijkstra、Bellman-Ford
最小生成树
Kruskal、Prim
NP完全问题
问题分类与归约

本文内容一览(快速理解)

  1. 渐进记号(O、Ω、Θ):描述算法复杂度的上界、下界和精确界
  2. 主方法(Master Method):快速求解分治算法递推关系的三条规则
  3. 排序算法:基于比较的排序(插入、堆、归并、快速)和不基于比较的排序(计数、基数、桶)
  4. 前k个最大数:重复找最大、Select算法、堆方法、锦标赛树等多种方法
  5. 算法设计方法:分治法(自顶而下)、动态规划(自底而上)、贪心法(局部最优)
  6. 图的周游算法:BFS(广度优先)、DFS(深度优先)、拓扑排序、连通分支分解
  7. 最短路径算法:Dijkstra(非负权值)、Bellman-Ford(允许负权值)
  8. 最小生成树算法:Kruskal(合并森林)、Prim(生长树)
  9. NP完全问题:P类、NP类、NP完全问题的特性与证明方法

一、算法分析基础(Algorithm Analysis Fundamentals):渐进记号与递推关系

这一章要建立的基础:理解如何描述和分析算法的复杂度,掌握渐进记号的含义和使用方法。

核心问题:如何描述算法的效率?如何分析分治算法的时间复杂度?


[!NOTE]
📝 关键点总结:算法分析是算法设计的基础。渐进记号(O、Ω、Θ)用于描述算法复杂度的上界、下界和精确界。主方法是快速求解分治算法递推关系的工具,有三条规则。

1.1 渐进记号(Asymptotic Notation):O、Ω、Θ的含义

概念的本质

渐进记号用于描述函数在nn趋于无穷大时的增长趋势:

  • 大O记号(O):表示"阶不高于",用于描述上界

    • 定义:如果存在常数c>0c > 0n0n_0,使得对所有nn0n \geq n_0都有f(n)cg(n)f(n) \leq c \cdot g(n),则f(n)=O(g(n))f(n) = O(g(n))
    • 用途:描述算法最坏情况复杂度
  • 大Ω记号(Ω):表示"阶不低于",用于描述下界

    • 定义:如果存在常数c>0c > 0n0n_0,使得对所有nn0n \geq n_0都有f(n)cg(n)f(n) \geq c \cdot g(n),则f(n)=Ω(g(n))f(n) = \Omega(g(n))
    • 用途:描述算法最好情况复杂度
  • 大Θ记号(Θ):表示"同阶",用于精确描述

    • 定义:如果f(n)=O(g(n))f(n) = O(g(n))f(n)=Ω(g(n))f(n) = \Omega(g(n)),则f(n)=Θ(g(n))f(n) = \Theta(g(n))
    • 用途:精确描述算法复杂度

图解说明

渐进记号
O (上界)
最坏情况
Ω (下界)
最好情况
Θ (同阶)
精确描述
比较函数增长

💡 说明:函数增长顺序从慢到快:O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ) < O(n!)。比较函数渐进阶的方法包括:极限法、定义证明、利用已知关系。

实际例子

渐进记号示例:

例1:n³ + 2n + 5 = O(n³)
- 当n ≥ 1时,n³ + 2n + 5 ≤ 8n³
- 所以是O(n³)(取c = 8, n₀ = 1)

例2:n² = Ω(n log n)
- 当n足够大时,n² ≥ n log n
- 所以是Ω(n log n)

例3:3n + 5 = Θ(n)
- 因为3n + 5 = O(n)且3n + 5 = Ω(n)
- 所以是Θ(n)

 


1.2 主方法(Master Method):快速求解递推关系

概念的本质

主方法是快速求解标准递推关系T(n)=aT(n/b)+f(n)T(n) = aT(n/b) + f(n)的工具,有三条规则:

规则1:如果f(n)=O(nkϵ)f(n) = O(n^{k-\epsilon})ϵ>0\epsilon > 0),则T(n)=Θ(nk)T(n) = \Theta(n^k)

  • 含义:f(n)f(n)nkn^k小很多,复杂度由nkn^k决定

规则2:如果f(n)=Θ(nk)f(n) = \Theta(n^k),则T(n)=Θ(nklogn)T(n) = \Theta(n^k \log n)

  • 含义:f(n)f(n)nkn^k同阶,复杂度是nklognn^k \log n

规则3:如果f(n)=Ω(nk+ϵ)f(n) = \Omega(n^{k+\epsilon})且满足正则条件af(n/b)cf(n)af(n/b) \leq cf(n)c<1c < 1),则T(n)=Θ(f(n))T(n) = \Theta(f(n))

  • 含义:f(n)f(n)nkn^k大很多,复杂度由f(n)f(n)决定

其中k=logbak = \log_b a

图解说明

f(n) << n^k
f(n) = n^k
f(n) >> n^k
递推关系T(n) = aT(n/b) + f(n)
计算k = log_b a
比较f(n)与n^k
规则1
T(n) = Θ(n^k)
规则2
T(n) = Θ(n^k log n)
规则3
T(n) = Θ(f(n))

💡 说明:主方法适用于标准形式的递推关系。如果不满足任何规则,需要使用替换法或序列求和法。归并排序:T(n) = 2T(n/2) + O(n),k=1,f(n)=n=Θ(n¹),满足规则2,所以T(n) = Θ(n log n)。

类比理解

就像判断两个数的大小关系:

  • 规则1f(n)f(n)nkn^k小很多 → 复杂度由nkn^k决定
  • 规则2f(n)f(n)nkn^k差不多 → 复杂度是nklognn^k \log n
  • 规则3f(n)f(n)nkn^k大很多 → 复杂度由f(n)f(n)决定

实际例子

主方法应用示例:

例1:T(n) = 2T(n/2) + n
- a=2, b=2, k=log₂2=1
- f(n)=n=Θ(n¹)=Θ(n^k)
- 满足规则2,所以T(n) = Θ(n log n)
- 应用:归并排序

例2:T(n) = 9T(n/3) + n log n
- a=9, b=3, k=log₃9=2
- f(n)=n log n=O(n^1.8)(取ε=0.2)
- 满足规则1,所以T(n) = Θ(n²)

例3:T(n) = 3T(n/2) + n²
- a=3, b=2, k=log₂3≈1.58
- f(n)=n²=Ω(n^1.78)(取ε=0.2)
- 检查正则条件:3(n/2)² ≤ (3/4)n²,满足
- 满足规则3,所以T(n) = Θ(n²)

 


二、排序算法(Sorting Algorithms):基于比较与非基于比较

这一章要建立的基础:理解排序是算法的基础操作,掌握不同排序算法的原理和特性。

核心问题:如何高效地对数据进行排序?有哪些不同的排序方法?


[!NOTE]
📝 关键点总结:排序算法分为基于比较的排序(时间复杂度下界为Ω(n log n))和不基于比较的排序(时间复杂度可优于O(n log n),但往往有额外条件限制)。前k个最大数问题有多种解决方法,需要根据n和k的关系选择最佳算法。

2.1 基于比较的排序算法(Comparison-Based Sorting):Ω(n log n)下界

概念的本质

基于比较的排序算法通过比较元素大小来决定相对位置,时间复杂度下界为Ω(nlogn)\Omega(n \log n)

主要算法

  1. 插入排序(Insertion Sort)

    • 时间复杂度:O(n2)O(n^2)
    • 特点:简单、稳定、原地排序
    • 适用:小规模数据或基本有序的数据
  2. 堆排序(Heapsort)

    • 时间复杂度:O(nlogn)O(n \log n)
    • 特点:原地排序但不稳定
    • 适用:需要原地排序且对稳定性要求不高的场景
  3. 归并排序(Merge Sort)

    • 时间复杂度:O(nlogn)O(n \log n)
    • 特点:稳定排序但需要额外O(n)O(n)空间
    • 适用:需要稳定排序且空间充足的场景
  4. 快速排序(Quick Sort)

    • 平均时间复杂度:O(nlogn)O(n \log n),最坏O(n2)O(n^2)
    • 特点:原地排序、平均性能最佳但不稳定
    • 适用:一般情况下的排序,平均性能最好

图解说明

基于比较的排序
插入排序
O(n²)
堆排序
O(n log n)
归并排序
O(n log n)
快速排序
平均O(n log n)
简单稳定
原地但不稳定
稳定但需空间
平均性能最佳

💡 说明:任何基于比较的排序算法至少需要lg(n!) ≈ n log n次比较。快速排序的平均性能最好(系数1.39),归并排序的系数为1但需要额外空间。

类比理解

就像整理书籍:

  • 插入排序:一本一本地插入到正确位置(简单但慢)
  • 堆排序:建立书架,每次取最上面的书(需要维护书架)
  • 归并排序:分成两堆,分别整理后合并(需要额外空间)
  • 快速排序:选一个参考书,小的放左边,大的放右边(平均最快)

实际例子

排序算法比较:

数组:[9, 6, 2, 4, 1, 5, 3, 8]

插入排序:逐步插入,O(n²)
堆排序:建堆后逐个取出,O(n log n)
归并排序:分治合并,O(n log n),稳定
快速排序:划分后递归,平均O(n log n),最快

选择建议:
- 小规模数据:插入排序
- 需要稳定:归并排序
- 一般情况:快速排序
- 空间受限:堆排序或快速排序

 


2.2 不基于比较的排序算法(Non-Comparison Sorting):突破O(n log n)

概念的本质

不基于比较的排序算法可以突破比较排序的Ω(nlogn)\Omega(n \log n)下界,时间复杂度可达到O(n)O(n),但往往有额外的条件限制。

主要算法

  1. 计数排序(Counting Sort)

    • 时间复杂度:O(n)O(n)
    • 要求:整数且范围有限(0aik0 \leq a_i \leq kk=O(n)k = O(n)
    • 特点:稳定排序
  2. 基数排序(Radix Sort)

    • 时间复杂度:O(d(n+k))O(d(n+k)),通常O(n)O(n)
    • 要求:dd位整数,每位取值范围为常数kk
    • 特点:稳定排序,从低位到高位逐位排序
  3. 桶排序(Bucket Sort)

    • 时间复杂度:O(n)O(n)(平均情况)
    • 要求:数据在[0,1)[0,1)范围内
    • 特点:分桶后对每个桶排序

图解说明

不基于比较的排序
计数排序
O(n)
基数排序
O(n)
桶排序
O(n)
要求:整数且范围有限
要求:d位整数
要求:数据在[0,1)

💡 说明:不基于比较的排序算法虽然时间复杂度低,但都有额外的条件限制。当数据满足这些条件时,可以使用这些算法获得更好的性能。

类比理解

就像分类整理:

  • 计数排序:统计每种物品的数量,然后按数量摆放(需要知道物品种类)
  • 基数排序:先按个位分类,再按十位分类,最后按百位分类(需要知道位数)
  • 桶排序:把物品放到不同的桶里,每个桶内再排序(需要知道物品的范围)

实际例子

不基于比较排序示例:

计数排序:
输入:[4, 5, 3, 0, 2, 3, 4, 2],k=5
统计:C[0]=1, C[2]=2, C[3]=2, C[4]=2, C[5]=1
输出:[0, 2, 2, 3, 3, 4, 4, 5]

基数排序:
输入:[329, 457, 657, 839, 436, 720, 355]
按个位:[720, 355, 436, 457, 657, 329, 839]
按十位:[720, 329, 436, 839, 355, 457, 657]
按百位:[329, 355, 436, 457, 657, 720, 839]

桶排序:
输入:[0.78, 0.17, 0.39, 0.26, 0.72, 0.94, ...]
分桶后对每个桶排序,最后串联

 


2.3 前k个最大数问题(Finding Top k Largest Numbers):多种解决方法

概念的本质

nn个数中找出前kk个最大的数是一个重要的实际问题,有多种解决方法,需要根据nnkk的关系选择最佳算法。

主要方法

  1. 重复找最大O(kn)O(kn)

    • 简单但效率低,适合kk很小的情况
  2. Select算法+比较O(n)O(n)

    • 先用Select算法找第kk顺序数,再与所有数比较
    • 实现复杂但时间复杂度好
  3. 堆方法

    • 最小堆O(nlogk)O(n \log k),空间O(k)O(k),适合kk较小
    • 最大堆O(n+klogn)O(n + k \log n),空间O(n)O(n),适合kk较小
  4. 锦标赛树O(n+klogn)O(n + k \log n),空间O(n)O(n)

    • 时间效率最高(约nn次比较),但空间开销大

图解说明

前k个最大数
k很小?
k << n
最小堆或
锦标赛树
k中等?
k ≈ n/2
Select算法
k很大?
k ≈ n
归并排序

💡 说明:实际应用中,根据n和k的关系选择最佳算法。例如,从十亿个数字中找出前一千个最大的数字,锦标赛树方法最合适(约n次比较)。

类比理解

就像选秀比赛:

  • 重复找最大:每次选第一名,重复kk次(简单但慢)
  • Select算法:先找到第kk名的分数线,再找出所有超过分数线的(需要先排序)
  • 堆方法:维护一个"前kk名"的名单,每次遇到更好的就替换最差的(空间效率高)
  • 锦标赛树:像锦标赛一样,每次决出冠军,然后快速决出第二名(时间效率最高)

实际例子

前k个最大数方法比较:

场景:n = 10⁹,k = 1000

方法1:重复找最大
- 时间复杂度:O(kn) = O(10¹²)
- 评价:太慢

方法2:Select+比较
- 时间复杂度:O(n) = O(10⁹)
- 评价:好,但实现复杂

方法3:最小堆
- 时间复杂度:O(n log k) = O(10⁹ × 10) = O(10¹⁰)
- 空间复杂度:O(k) = O(1000)
- 评价:空间效率高

方法4:锦标赛树
- 时间复杂度:O(n + k log n) ≈ O(n) = O(10⁹)
- 空间复杂度:O(n) = O(10⁹)
- 评价:时间效率最高,但空间开销大

推荐:锦标赛树方法(时间更重要)

 


三、算法设计方法(Algorithm Design Paradigms):分治、动态规划、贪心

这一章要建立的基础:理解三种重要的算法设计方法,掌握它们的原理、区别和应用场景。

核心问题:如何设计高效的算法?有哪些经典的算法设计方法?


[!NOTE]
📝 关键点总结:分治法、动态规划、贪心法是三种重要的算法设计方法。分治法是自顶而下,子问题独立;动态规划是自底而上,子问题有重叠;贪心法是每一步做最优选择,不需要解决所有子问题。

3.1 分治法(Divide and Conquer):自顶而下的分解

概念的本质

分治法将大问题分解为小问题,递归求解,最后合并结果。核心是"底、分、合"三个步骤。

特点

  • 自顶而下:从原问题开始,逐步分解为子问题
  • 子问题独立:同层的子问题之间相互独立、互不相交
  • 相同规则:每层递归都必须遵循相同的分解规则
  • 递归求解:用完全同样的方法递归解出子问题

图解说明

原问题
规模n
分解
Divide
子问题1
规模n/b
子问题2
规模n/b
子问题a
规模n/b
递归求解
Conquer
合并结果
Combine
原问题的解

💡 说明:分治法要求子问题之间互不相交、相互独立。应用:归并排序、快速排序、二元搜索。时间复杂度通常可以用主方法求解。

类比理解

就像整理图书馆:

  1. 分解:将图书馆分成几个区域
  2. 递归:对每个区域用同样的方法继续细分和整理
  3. 合并:当每个小区域都整理好后,整个图书馆就整理完成了

实际例子

分治法应用:

归并排序:
- 分:将数组分为两半
- 递归:分别排序两半
- 合:合并两个有序序列
- 时间复杂度:O(n log n)

快速排序:
- 分:选择主元,划分数组
- 递归:分别排序左右两部分
- 合:不需要合并(原地排序)
- 平均时间复杂度:O(n log n)

二元搜索:
- 分:取中间元素,将数组分为两部分
- 递归:在相应部分中查找
- 合:直接返回结果
- 时间复杂度:O(log n)

 


3.2 动态规划(Dynamic Programming):自底而上的优化

概念的本质

动态规划通过自底而上的方式,枚举所有可能的分解情况,利用子问题的最优解来构造原问题的最优解。

前提条件

  1. 最优子结构特性:原问题的最优解是由其子问题的最优解构成的
  2. 重叠子问题:问题的子问题之间有交集(subproblems share subsubproblems)

特点

  • 自底而上:需要先解决所有子问题,然后逐步构造更大规模问题的解
  • 查表法:存储子问题的解,避免重复计算
  • 枚举所有可能:需要枚举所有可能的分解方式

图解说明

动态规划步骤
初始化
S₀:最小规模问题的解
归纳步骤1
S₁:用S₀求解
归纳步骤2
S₂:用S₀,S₁求解
...
归纳步骤n
Sₙ:用S₀...Sₙ₋₁求解
原问题的解
查表法
存储所有Sᵢ的解
需要时直接查表

💡 说明:动态规划需要枚举所有可能的分解情况,这是因为全局最优解到底用到什么组合,在得到最优解之前,我们预先是不知道的。应用:矩阵连乘、最长公共子序列、0/1背包。

类比理解

就像建房子:

  • 最优子结构:整栋房子的最优设计 = 每个房间的最优设计 + 房间之间的最优连接
  • 重叠子问题:多个房间可能使用相同的设计模式
  • 自底而上:先设计好每个房间,再考虑如何连接它们
  • 查表法:把设计好的房间方案记录下来,需要时直接查表

实际例子

动态规划应用:

矩阵连乘问题:
- 问题:n个矩阵要连乘,找到最优的相乘顺序
- 最优子结构:A₁...Aₙ的最优顺序 = A₁...Aₖ的最优顺序 + Aₖ₊₁...Aₙ的最优顺序 + 合并代价
- 重叠子问题:A₁...Aₖ的最优顺序会被多次使用
- 时间复杂度:O(n³)

斐波那契数列:
- 分治法(递归):O(2ⁿ),重复计算
- 动态规划:O(n),存储已计算的F(i)
- 对比:动态规划避免了重复计算

 


3.3 贪心法(Greedy Algorithm):局部最优的选择

概念的本质

贪心法在每一步都做出当前最优选择,希望通过局部最优选择得到全局最优解。

前提条件

  1. 最优子结构特性:原问题的最优解可以通过合并子问题的最优解而得到
  2. 贪心选择性质:局部最优选择可以导致全局最优解

特点

  • 贪心选择:每一步都做出当前最优选择
  • 不需要解决所有子问题:只关注当前步骤的最优选择
  • 不保证最优:在某些问题中可以得到最优解,但不是所有问题都适用

图解说明

问题
第1步:选择当前最优
第2步:选择当前最优
第3步:选择当前最优
...
第n步:选择当前最优
得到解
贪心选择性质
局部最优
→ 全局最优

💡 说明:贪心法通常不保证最优解,但在某些问题中可以得到最优解。应用:区间调度、最优邮局设置、霍夫曼编码。证明贪心法的正确性通常使用反证法或交换论证。

类比理解

就像在迷宫中找出口:

  • 贪心策略:每一步都选择离出口最近的方向
  • 局部最优:当前这一步看起来最好的选择
  • 全局最优:如果贪心策略正确,最终能找到最优路径

实际例子

贪心法应用:

区间调度问题:
- 问题:n门课申请使用同一间教室,每门课有开始和结束时间
- 贪心策略:按结束时间从早到晚排序,每次选择结束时间最早且不冲突的活动
- 结果:可以得到最优解
- 时间复杂度:O(n log n)(排序)

最优邮局设置:
- 问题:在街上建邮局,使得任意一户人家到最近邮局的距离不超过100米
- 贪心策略:第一个邮局建在H[1]+100(最远位置),然后重复
- 结果:可以得到最优解
- 时间复杂度:O(n)

找零钱问题:
- 问题:用最少的硬币找零
- 贪心策略:每次选择面额最大的硬币
- 结果:在某些情况下可能不是最优的
- 注意:贪心法不总是最优

 


3.4 三种方法的比较(Comparison):理解区别和联系

概念的本质

三种方法都用于解决优化问题,都利用了最优子结构特性,但分解方式、子问题关系、求解顺序不同。

比较表

特性 分治法 动态规划 贪心法
分解方式 固定规则分解 枚举所有可能分解 贪心选择分解
子问题关系 相互独立、互不相交 有重叠、有交集 不需要解决所有子问题
求解顺序 自顶而下 自底而上 按贪心策略顺序
查表法 不需要 需要(存储子问题解) 不需要
保证最优 保证 保证 不一定保证

图解说明

三种方法比较
分治法
自顶而下
子问题独立
动态规划
自底而上
子问题重叠
贪心法
贪心选择
局部最优
固定分解规则
枚举所有分解
贪心选择分解

💡 说明:选择方法时,如果子问题有重叠,用动态规划;如果贪心选择性质成立,用贪心法(更简单);如果子问题独立,用分治法。

类比理解

就像三种不同的策略:

  • 分治法:按照固定的规则切蛋糕(每次都从中间切),切好的每一块都是独立的
  • 动态规划:建房子,先打好所有地基(解决所有子问题),然后一层层往上建
  • 贪心法:在迷宫中,每一步都选择看起来最好的方向,不需要考虑所有可能的路径

实际例子

方法选择示例:

问题1:归并排序
- 子问题独立 → 分治法
- 时间复杂度:O(n log n)

问题2:矩阵连乘
- 子问题重叠 → 动态规划
- 时间复杂度:O(n³)

问题3:区间调度
- 贪心选择性质成立 → 贪心法
- 时间复杂度:O(n log n)(排序)

问题4:0/1背包
- 子问题重叠 → 动态规划
- 贪心法不适用(不满足贪心选择性质)

 


四、图的周游算法(Graph Traversal Algorithms):BFS、DFS及其应用

这一章要建立的基础:理解图的周游算法是图算法的基础,掌握BFS和DFS的原理和应用。

核心问题:如何系统地访问图中的所有顶点和边?如何利用周游算法解决实际问题?


[!NOTE]
📝 关键点总结:图的周游是对一个图的每个顶点及每条边按某种顺序逐一访问。BFS按跳数距离逐层向外展开搜索,DFS尽可能深地搜索然后回溯。拓扑排序和连通分支分解是DFS的重要应用。

4.1 广度优先搜索(BFS):按距离逐层搜索

概念的本质

BFS根据顶点到源点的跳数距离,一层一层地、逐层向外展开搜索。使用队列(FIFO)存储灰色顶点。

算法特点

  • 逐层展开:首先访问距离为0的顶点(源点),然后是距离为1的,再然后是距离为2的
  • 使用队列:采用先进先出(FIFO)的队列存储灰色顶点
  • 复杂度O(n+m)O(n+m),其中nn是顶点数,mm是边数

图解说明

源点s
距离0
第1层
距离1
第2层
距离2
第3层
距离3
...

💡 说明:BFS树 = 最短距离树(假定每条边长度为1)。应用:最短路径(边权为1)、图的二着色。任何周游算法都必须访问图中每个顶点和边,因此BFS是一个最优算法。

类比理解

就像水波扩散:

  • 在池塘中心扔一块石头(源点ss
  • 水波一层一层向外扩散
  • 每一层代表距离中心相同距离的所有点

实际例子

BFS应用:

最短路径(边权为1):
- BFS可以找到从源点到所有顶点的最短路径
- 时间复杂度:O(n+m)

图的二着色:
- 用红、蓝两色为图着色
- 起始顶点着红色,邻居着相反颜色
- 如果发现相邻顶点颜色相同,说明存在奇回路,图不可二着色
- 时间复杂度:O(n+m)

 


4.2 深度优先搜索(DFS):尽可能深地搜索

概念的本质

DFS尽可能深地搜索图,然后回溯。使用递归或栈实现。

算法特点

  • 深度搜索:选择一个未访问的邻居,继续深度搜索
  • 回溯:当从某个顶点出发的所有访问完成后,回溯到父节点
  • 时间戳:每个顶点有两个时间戳:发现时刻d[u]d[u]和完成时刻f[u]f[u]
  • 复杂度O(n+m)O(n+m)

图解说明

访问s
选择未访问的邻居v
递归访问v
v的所有邻居
都已访问?
继续从v深度搜索
回溯到父节点
父节点还有
未访问邻居?
继续回溯

💡 说明:DFS会导致图中边的分类:树边、反向边、前向边、交叉边。如果vvuu的儿子,那么[d(v),f(v)][d(u),f(u)][d(v), f(v)] \subseteq [d(u), f(u)]。应用:拓扑排序、强连通分支分解。

类比理解

就像走迷宫:

  • 选择一条路一直走到头
  • 如果走到死胡同,就回溯到上一个路口
  • 尝试另一条路
  • 直到探索完所有路径

实际例子

DFS应用:

拓扑排序:
- 对有向无环图(DAG)的顶点排序
- 基于DFS,按完成时刻从大到小排序
- 应用:课程安排、任务调度
- 时间复杂度:O(n+m)

强连通分支分解:
- 将有向图的顶点划分为不相交的强连通分支
- 两轮DFS:原图标记时间戳,转置图按时间戳从大到小DFS
- 应用:网络分析、社交网络
- 时间复杂度:O(n+m)

 


五、最短路径算法(Shortest Path Algorithms):Dijkstra与Bellman-Ford

这一章要建立的基础:理解单源最短路径问题是图算法中的经典问题,掌握两种主要算法的原理和适用场景。

核心问题:如何找到从源点到所有其他顶点的最短路径?不同算法适用于什么情况?


[!NOTE]
📝 关键点总结:单源最短路径问题是给定图GG中一个顶点ss,求解从ss到图中所有其它顶点的最短路径。Dijkstra算法要求边权为非负实数,时间复杂度O(n2)O(n^2)O(mlogn)O(m \log n)。Bellman-Ford算法允许负权值但不能有负回路,时间复杂度O(nm)O(nm)

5.1 Dijkstra算法:贪心法求解最短路径

概念的本质

Dijkstra算法采用贪心法,每次选择距离源点最近的未确定顶点,逐步构造最短路径树。

算法特点

  • 贪心选择:每次从灰色节点集中选择d[]d[\cdot]最小的一个,将其变成黑色
  • 松弛操作:通过新确定的黑色节点,松弛其所有出行边
  • 要求:边权为非负实数
  • 复杂度:使用数组O(n2)O(n^2),使用堆O(mlogn)O(m \log n)

图解说明

初始化
s变黑,d[s]=0
s的邻居变灰
选择d值最小的灰色节点u
u变黑
最短路径确定
松弛u的所有邻居v
还有灰色节点?
所有最短路径确定

💡 说明:Dijkstra算法的做法与Prim算法几乎完全一样。Prim算法构造最小生成树,而Dijkstra算法构造最短路径树。应用:路由算法(OSPF)、网络优化。

类比理解

就像水波扩散,但考虑权重:

  • 从源点开始,距离为0
  • 每次选择距离源点最近的未确定点
  • 通过这个点更新其邻居的距离
  • 逐步向外扩展,直到所有点都确定

实际例子

Dijkstra算法执行:

图:s --3--> a --2--> b
    |        |
    1        4
    |        |
    v        v
    c --5--> d

执行过程:
1. s变黑,d[s]=0,松弛:d[a]=3, d[c]=1
2. c变黑(d[c]最小=1),松弛:d[d]=6
3. a变黑(d[a]最小=3),松弛:d[b]=5, d[d]=min(6,7)=6
4. b变黑(d[b]最小=5)
5. d变黑(d[d]=6)

最短路径:
- s → c:1
- s → c → d:6
- s → a:3
- s → a → b:5

 


5.2 Bellman-Ford算法:动态规划求解最短路径

概念的本质

Bellman-Ford算法采用动态规划,进行n1n-1轮松弛操作,允许负权值但不能有负回路。

算法特点

  • 多轮松弛:进行n1n-1轮,每轮对所有边进行松弛
  • 负回路检测:如果第nn轮还能松弛,说明存在负回路
  • 允许负权值:可以处理有负权边的图
  • 复杂度O(nm)O(nm)

图解说明

初始化
d[s]=0, 其他=∞
第1轮:松弛所有边
第2轮:松弛所有边
...
第n-1轮:松弛所有边
第n轮:检测负回路
还能松弛?
报告负回路
返回最短路径

💡 说明:Bellman-Ford算法采用动态规划的思想。在每一轮中,d[v]d[v]表示从ssvv最多经过kk条边的最短路径长度。应用:检测负回路、处理负权边(BGP路由协议)。

类比理解

就像逐步放宽限制:

  • 第1轮:只允许经过1条边
  • 第2轮:允许经过2条边
  • n1n-1轮:允许经过n1n-1条边(最多)
  • 如果还能继续改进,说明有负回路

实际例子

Bellman-Ford算法执行:

图:s --1--> a --(-2)--> b --3--> c

第1轮(最多1条边):
- (s,a): d[a] = 1
- 其他:不变

第2轮(最多2条边):
- (a,b): d[b] = 1+(-2) = -1
- 其他:不变

第3轮(最多3条边):
- (b,c): d[c] = -1+3 = 2
- 其他:不变

第4轮:无更新

结果:最短路径
- s → a:1
- s → a → b:-1
- s → a → b → c:2

如果存在负回路,第n轮还能继续更新

 


5.3 两种算法的比较(Comparison)

比较表

特性 Dijkstra算法 Bellman-Ford算法
设计方法 贪心法 动态规划
边权限制 非负实数 允许负权值
负回路 不能处理 能检测
时间复杂度 O(n2)O(n^2)O(mlogn)O(m \log n) O(nm)O(nm)
适用场景 非负权图,稠密图 有负权边,需要检测负回路

图解说明

选择算法
有负权边?
Dijkstra算法
更快
需要检测负回路?
Bellman-Ford算法
图是DAG?
拓扑排序+DP
O(n+m)

💡 说明:如果图是有向无环图(DAG),可以使用拓扑排序结合动态规划,时间复杂度为O(n+m)O(n+m),比Bellman-Ford更快。

实际例子

算法选择示例:

场景1:路由算法(网络图,边权为延迟)
- 边权:非负(延迟不能为负)
- 推荐:Dijkstra算法
- 理由:更快,O(m log n)

场景2:金融网络(可能有负权边表示收益)
- 边权:可能有负值
- 推荐:Bellman-Ford算法
- 理由:能处理负权边,能检测负回路(套利机会)

场景3:任务调度(DAG,边权为时间)
- 图:有向无环图
- 推荐:拓扑排序+DP
- 理由:最快,O(n+m)

 


六、最小生成树算法(Minimum Spanning Tree Algorithms):Kruskal与Prim

这一章要建立的基础:理解最小生成树是图算法中的经典问题,掌握两种主要算法的原理和实现。

核心问题:如何找出一棵生成树,使得所有边的总权值最小?


[!NOTE]
📝 关键点总结:最小生成树(MST)是加权图的一棵生成树,其所有边的总权值最小。Kruskal算法按边权从小到大,合并不同的连通分支。Prim算法从单个顶点开始,每次添加距离树最近的顶点。

6.1 Kruskal算法:合并森林

概念的本质

Kruskal算法按边权从小到大排序,每次选择一条边,如果它连接两个不同的连通分支,则加入MST。

算法特点

  • 按边权排序:将所有边按权重从小到大排序
  • 合并连通分支:使用并查集(Union-Find)数据结构
  • 复杂度O(mlogn)O(m \log n),主要时间花在排序上
  • 适合:稀疏图

图解说明

所有边按权重排序
选择权重最小的边
连接不同连通分支?
加入MST
合并连通分支
跳过(形成回路)
还有未处理的边?
MST完成

💡 说明:Kruskal算法使用并查集数据结构来高效地判断两个顶点是否在同一个连通分支中。应用:网络设计、电路设计。

类比理解

就像连接城市:

  • 把所有道路按长度排序
  • 从最短的道路开始,如果它连接两个还没有连通的城市,就修建这条道路
  • 重复直到所有城市都连通

实际例子

Kruskal算法执行:

图:a --5-- b
    |       |
    3       2
    |       |
    c --1-- d

边排序:cd(1), bd(2), ac(3), ab(5)

第1步:选择cd(1),加入MST
第2步:选择bd(2),加入MST
第3步:选择ac(3),加入MST
第4步:选择ab(5),但a和b已连通,跳过

MST:{cd, bd, ac},总权值6

 


6.2 Prim算法:生长树

概念的本质

Prim算法从单个顶点开始,每次添加距离树最近的顶点,逐步生长成一棵MST。

算法特点

  • 从单个顶点开始:选择任意一个顶点作为起始点
  • 贪心选择:每次选择距离树最近的顶点
  • 复杂度:使用数组O(n2)O(n^2),使用堆O(mlogn)O(m \log n)
  • 适合:稠密图

图解说明

选择起始顶点s
初始化:树={s}
选择距离树最近的顶点u
将u和边(u,v)加入树
更新距离树最近的顶点
所有顶点都在树中?
MST完成

💡 说明:Prim算法的做法与Dijkstra算法几乎完全一样。Prim算法构造最小生成树,而Dijkstra算法构造最短路径树。应用:网络设计、电路设计。

类比理解

就像种树:

  • 从一颗种子(起始顶点)开始
  • 每次选择离树最近的种子,让它长成树的一部分
  • 重复直到所有种子都长成树

实际例子

Prim算法执行:

图:a --5-- b
    |       |
    3       2
    |       |
    c --1-- d

从a开始:
1. 树={a},最近:c(距离3)
2. 树={a,c},最近:d(距离1,通过c)
3. 树={a,c,d},最近:b(距离2,通过d)
4. 树={a,b,c,d},完成

MST:{ac, cd, bd},总权值6

 


6.3 两种算法的比较(Comparison)

比较表

特性 Kruskal算法 Prim算法
设计方法 贪心法(按边权) 贪心法(按顶点)
数据结构 并查集 优先队列或数组
时间复杂度 O(mlogn)O(m \log n) O(n2)O(n^2)O(mlogn)O(m \log n)
适合场景 稀疏图 稠密图
实现难度 中等(需要并查集) 简单

图解说明

选择算法
图是稀疏图?
m << n²
Kruskal算法
O(m log n)
图是稠密图?
m ≈ n²
Prim算法
O(n²)
都可以
看实现难度

💡 说明:两种算法都能得到最小生成树。Kruskal算法适合稀疏图,Prim算法适合稠密图。如果图是稠密图,Prim算法使用数组实现更快。

实际例子

算法选择示例:

场景1:稀疏图(n=1000, m=2000)
- 推荐:Kruskal算法
- 理由:O(m log n) = O(2000 × 10) = O(20000)
- Prim(堆):O(m log n) = O(20000)
- Prim(数组):O(n²) = O(1000000)
- Kruskal更快

场景2:稠密图(n=1000, m=500000)
- 推荐:Prim算法(数组)
- 理由:O(n²) = O(1000000)
- Kruskal:O(m log n) = O(500000 × 10) = O(5000000)
- Prim更快

 


七、NP完全问题(NP-Complete Problems):问题分类与归约

这一章要建立的基础:理解NP完全问题是计算复杂性理论的核心,掌握问题分类和归约方法。

核心问题:如何判断一个问题的计算难度?如何证明一个问题是否是NP完全的?


[!NOTE]
📝 关键点总结:判定型问题的答案只有是或否。P类是多项式时间可判定的问题,NP类是多项式时间可检验的问题。NP完全问题是NP类中最难的问题,如果有一个NP完全问题有多项式算法,则P=NP。多项式归约是证明问题难度的工具。

7.1 问题分类(Problem Classification):P类、NP类、NP完全

概念的本质

判定型问题(Decision Problem)

  • 答案只有两种:是(yes)或否(no)
  • 例如:判断"一个图是否存在一条哈密尔顿回路"

优化型问题(Optimization Problem)

  • 问题的解对应于一个最优的数值
  • 例如:在两点之间找一条最短路径
  • 可以转化为判定型问题

问题分类

  1. P类(Polynomial Time):多项式时间可判定的问题

    • 存在多项式时间的算法可以判定
    • 例如:排序、最短路径、最小生成树
  2. NP类(Nondeterministic Polynomial Time):多项式时间可检验的问题

    • 如果答案是"是",存在一个"证书",可以在多项式时间内验证
    • 例如:哈密尔顿回路问题、3-SAT问题
  3. NP完全问题(NP-Complete):NP类中最难的问题

    • 属于NP类
    • 所有NP类问题都可以多项式归约到它
    • 如果有一个NP完全问题有多项式算法,则P=NP

图解说明

问题分类
P类
多项式时间可判定
NP类
多项式时间可检验
NP完全问题
NP类中最难的
如果有一个NP完全问题
有多项式算法
则P=NP

💡 说明:P ⊆ NP(如果一个问题可以在多项式时间内判定,那么它的答案肯定可以在多项式时间内验证)。P=NP?这是计算机科学中最重要的未解决问题之一。

类比理解

就像考试:

  • P类:可以在多项式时间内直接解答的问题(简单题)
  • NP类:如果给你答案,可以在多项式时间内验证是否正确(验证题)
  • NP完全:NP类中最难的问题,如果解决了它,就解决了所有NP类问题(最难题)

实际例子

问题分类示例:

P类问题:
- 排序:O(n log n)
- 最短路径:O(n²)或O(m log n)
- 最小生成树:O(m log n)

NP类问题:
- 哈密尔顿回路:给定一个图,是否存在一条经过每个顶点恰好一次的回路?
- 3-SAT:给定一个3-CNF公式,是否存在一个赋值使其为真?

NP完全问题:
- 哈密尔顿回路问题
- 3-SAT问题
- 旅行商问题(TSP)
- 0/1背包问题

 


7.2 多项式归约(Polynomial Reduction):证明问题难度

概念的本质

多项式归约是证明问题难度的工具。如果问题π1\pi_1可以多项式归约到问题π2\pi_2(记作π1pπ2\pi_1 \leq_p \pi_2),则π2\pi_2至少和π1\pi_1一样难。

归约的含义

  • 如果π1pπ2\pi_1 \leq_p \pi_2,且π2\pi_2有多项式算法,则π1\pi_1也有多项式算法
  • 如果π1pπ2\pi_1 \leq_p \pi_2,且π1\pi_1是NP完全的,则π2\pi_2也是NP完全的

证明NP完全性的方法

  1. 证明问题属于NP类(多项式时间可检验)
  2. 选择一个已知的NP完全问题π\pi
  3. 证明πp\pi \leq_p该问题(多项式归约)

图解说明

证明问题π是NP完全的
步骤1:证明π属于NP类
步骤2:选择已知NP完全问题π₀
步骤3:证明π₀ ≤p π
结论:π是NP完全的

💡 说明:第一个NP完全问题是Cook-Levin定理证明的3-SAT问题。之后,可以通过归约证明其他问题是NP完全的。常见的NP完全问题包括:哈密尔顿回路、旅行商问题、0/1背包、图着色等。

类比理解

就像证明难度:

  • 如果问题A可以归约到问题B,说明B至少和A一样难
  • 如果A很难(NP完全),那么B也很难(NP完全)
  • 就像如果"解方程"可以归约到"解更复杂的方程",说明后者至少和前者一样难

实际例子

多项式归约示例:

已知:3-SAT是NP完全的

证明:哈密尔顿回路是NP完全的

步骤1:证明哈密尔顿回路属于NP类
- 给定一个回路,可以在O(n)时间内验证它是否经过每个顶点恰好一次

步骤2:选择已知NP完全问题:3-SAT

步骤3:证明3-SAT ≤p 哈密尔顿回路
- 构造一个图,使得该图有哈密尔顿回路当且仅当3-SAT公式可满足
- 这个构造可以在多项式时间内完成

结论:哈密尔顿回路是NP完全的

 


📝 本章总结

核心要点回顾

  1. 算法分析基础

    • 渐进记号(O、Ω、Θ)用于描述算法复杂度
    • 主方法快速求解分治算法的递推关系
    • 函数增长顺序:O(1) < O(log n) < O(n) < O(n log n) < O(n²) < O(2ⁿ)
  2. 排序算法

    • 基于比较的排序:插入、堆、归并、快速排序,下界Ω(n log n)
    • 不基于比较的排序:计数、基数、桶排序,可达到O(n)
    • 前k个最大数:根据n和k的关系选择最佳方法
  3. 算法设计方法

    • 分治法:自顶而下,子问题独立
    • 动态规划:自底而上,子问题重叠
    • 贪心法:局部最优选择,不一定保证全局最优
  4. 图的周游算法

    • BFS:按距离逐层搜索,O(n+m)
    • DFS:深度搜索然后回溯,O(n+m)
    • 应用:拓扑排序、连通分支分解
  5. 最短路径算法

    • Dijkstra:非负权值,O(n²)或O(m log n)
    • Bellman-Ford:允许负权值,O(nm),能检测负回路
  6. 最小生成树算法

    • Kruskal:按边权排序,合并连通分支,O(m log n)
    • Prim:从顶点开始,生长树,O(n²)或O(m log n)
  7. NP完全问题

    • P类:多项式时间可判定
    • NP类:多项式时间可检验
    • NP完全:NP类中最难的问题
    • 多项式归约:证明问题难度的工具

知识地图

算法课程
算法分析基础
渐进记号、主方法
排序算法
比较排序、非比较排序
算法设计方法
分治、动态规划、贪心
图算法
BFS、DFS
最短路径
Dijkstra、Bellman-Ford
最小生成树
Kruskal、Prim
NP完全问题
问题分类、归约
应用:算法复杂度分析
应用:图相关问题
应用:问题难度分析

关键决策点

  • 选择排序算法:根据数据特性(整数/实数、范围、稳定性要求)选择
  • 选择算法设计方法:根据子问题是否重叠、是否有贪心选择性质选择
  • 选择图的周游算法:BFS用于最短路径(边权为1),DFS用于拓扑排序、强连通分支
  • 选择最短路径算法:非负权值用Dijkstra,有负权边用Bellman-Ford
  • 选择最小生成树算法:稀疏图用Kruskal,稠密图用Prim
  • 判断问题难度:使用多项式归约证明NP完全性

💡 延伸学习:算法课程涵盖了算法设计与分析的核心内容。掌握这些知识点有助于我们:

  1. 理解算法复杂度的分析方法
  2. 选择合适的算法解决实际问题
  3. 设计高效的算法
  4. 理解计算复杂性理论的基础

📚 参考教程

本文基于以下教程文件整理:

  1. 要点2:比较不同函数的渐进阶大小 - 参考PPT:第 1 章-PPT-N2_v4
  2. 要点3:推导递推关系的渐进阶(主方法) - 参考PPT:第 1 章-PPT-N2_v4
  3. 要点4:排序算法和前k个最大数 - 参考PPT:第 4 章-PPT-N2_modified
  4. 要点5:贪心法、分治法、动态规划 - 参考PPT:第 6 章-PPT-N2_v2
  5. 要点6:BFS、DFS、拓扑排序、连通分支 - 参考PPT:第 8 章-PPT-N2
  6. 要点7:最短路算法(Dijkstra-Bellman-Ford) - 参考PPT:第 10 章-PPT-N2(1)
  7. 要点8:最小生成树算法(Kruskal-Prim) - 参考PPT:第 11 章-PPT-N2 v3.1
  8. 要点9:NP完全问题 - 参考PPT:第 13 章-PPT-N2

每个要点的详细内容请参考相应的教程文件。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

roman_日积跬步-终至千里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值