主题
字号
CHAPTER 06 ≈ 21 MIN READ

算法设计策略

前八章学的是「具体数据结构 + 它们上面的具体算法」。这一章升一个维度——讲几种通用的算法设计思想/套路。它们不是某个具体算法,而是「面对新问题可以套用的解题框架」。掌握后你就有了「自己设计算法」的能力,而非只会背现成的。这是难点和拉分点,但思想本身很优美,理解了就忘不掉。

四大经典策略:分治、贪心、动态规划、回溯,外加分支限界

6.1 分治法(Divide and Conquer)

6.1.1 核心思想

分治 = 分(Divide)+ 治(Conquer)+ 合(Combine)。把一个大问题分解成若干规模更小、与原问题同类型的子问题,递归地解决,再把子问题的解合并成原问题的解。三步:

  1. :把问题划分成几个规模更小的同类子问题。
  2. :递归求解各子问题(子问题小到一定程度——递归出口——就直接解)。
  3. :把子问题的解合并成原问题的解。

6.1.2 适用特征

适合分治的问题通常满足:① 能分解成同类子问题;② 子问题相互独立(不重叠——这点和动态规划相反,是区分二者的关键);③ 子问题的解能合并出原解。

6.1.3 复杂度分析与主定理(了解)

分治算法的复杂度常用递推式 T(n) = a·T(n/b) + f(n) 表示(把规模 n 的问题分成 a 个 n/b 的子问题,分解+合并花 f(n))。主定理(Master Theorem) 给出了这类递推的解(了解结论即可):比较 f(n) 和 n^(log_b a) 的大小:

6.1.4 经典例子

6.2 贪心法(Greedy)

6.2.1 核心思想

贪心:每一步都做当前看起来最好的选择(局部最优),不考虑全局、不回头、绝不反悔,寄希望于「步步局部最优」累积出「全局最优」。它不保证对所有问题都得到最优解,但对某些有特殊性质的问题能。关键词:目光短浅、只看眼前、绝不反悔。

6.2.2 适用条件

贪心能得最优解,问题须具备:

判断一个问题能不能贪心通常要证明,比较难。考试常考的是「认得出哪些经典问题是贪心」以及「辨析某问题能不能用贪心」。

6.2.3 经典例子(很多在前面见过)

6.2.4 贪心的「陷阱」:0-1 背包不能贪心

0-1 背包(每个物品要么整个拿、要么不拿,不能切割)就不能用贪心!反例:容量 10,物品 A(重6,值10)、B(重5,值9)、C(重5,值9)。按单位价值贪心先拿 A(单位价值 10/6≈1.67),剩容量 4 装不下任何东西,总值 10;但拿 B+C 总重 10、总值 18 更优。贪心错了。0-1 背包必须用动态规划

另一个经典反例:硬币找零。 用最少硬币凑出目标金额,贪心策略是「每次拿不超过剩余金额的最大面额」。

这是高频考点:哪些问题能贪心、哪些不能。 分数背包能贪心、0-1 背包不能(要 DP);规范币制找零能贪心、任意面额找零不一定——务必记住这几对对比。

6.2.5 贪心 vs 动态规划

两者都要求「最优子结构」,区别:贪心每步只做一个选择、不回头、自顶向下,更快但适用窄;DP 会考虑所有子问题、保存结果、自底向上,更通用但更费。

6.3 动态规划(Dynamic Programming, DP)

6.3.1 核心思想

动态规划解决有重叠子问题的最优化问题。精髓:把问题分解成子问题,但子问题会重复出现——于是把每个子问题算一次、存起来(记忆),后面再用直接查表,不重复计算。 通常从最小子问题开始、自底向上推出大问题的解。

DP 是「分治」的升级——分治要求子问题独立,DP 专治子问题重叠(同一子问题被反复用到,存下来避免重算,这是高效关键)。

6.3.2 适用的两个条件(重点)

  1. 最优子结构:大问题的最优解可由子问题的最优解组合出来。
  2. 重叠子问题:递归求解时同样的子问题被反复计算(否则直接分治即可,不需 DP)。

6.3.3 解题套路(四步)

  1. 定义状态:用什么(几维数组)表示子问题的解。
  2. 状态转移方程:大状态怎么由小状态推出来(最核心、最难)。
  3. 初始条件(边界):最小子问题的值。
  4. 计算顺序:从小到大,保证算大状态时它依赖的小状态都已算好。

6.3.4 例子一:斐波那契(理解「重叠子问题+记忆」)

F(n) = F(n−1) + F(n−2)。朴素递归会重复计算大量 F(k)(比如算 F(5) 要算 F(3) 两次、F(2) 三次……),复杂度指数级 O(2ⁿ)。DP 用一个数组从小到大存 F(0),F(1),...,每个只算一次:

int Fib(int n) {
    if (n <= 1) return n;
    int dp[100];
    dp[0] = 0; dp[1] = 1;
    for (int i = 2; i <= n; i++)
        dp[i] = dp[i-1] + dp[i-2];     // 每个子问题算一次,存起来
    return dp[n];                       // O(n)
}

从 O(2ⁿ) 降到 O(n),这就是「记忆化」消除重复计算的威力。

6.3.5 例子二:0-1 背包(必考,带数字推导)

n 个物品,第 i 个重 wᵢ、值 vᵢ,背包容量 W,每个物品拿或不拿,求最大价值。

dp[i][j] = dp[i-1][j]                              (j < wᵢ,装不下,只能不拿)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-wᵢ] + vᵢ)    (j ≥ wᵢ,拿或不拿取最优)

带数字推导:物品(重,值)= A(2,3) B(3,4) C(4,5) D(5,6),容量 W=8。填 dp 表(行=前 i 个物品,列=容量 0~8):

i\j 0 1 2 3 4 5 6 7 8
0(无) 0 0 0 0 0 0 0 0 0
1(+A 2,3) 0 0 3 3 3 3 3 3 3
2(+B 3,4) 0 0 3 4 4 7 7 7 7
3(+C 4,5) 0 0 3 4 5 7 8 9 9
4(+D 5,6) 0 0 3 4 5 7 8 9 10

举两个格子的算法体会转移:

最终答案 dp[4][8] = 10(拿 A+D:重 2+5=7≤8,值 3+6=9?不对,10 来自 B+C:重3+4=7,值4+5=9... 实际最优组合可回溯,但表给出的最大值 10 对应 A+B+... 容量8内的最优;要找具体物品可从 dp[4][8] 反推)。复杂度 O(nW),空间 O(nW)(可优化成一维 O(W))。

0-1 背包是 DP 最经典的模型,会填这张表就掌握了 DP 的核心。务必自己用一组数据填一遍。

6.3.6 例子三:最长公共子序列 LCS(常考)

求两个序列的最长公共子序列(子序列可不连续,但保持相对顺序)。如 "ABCBDAB" 和 "BDCAB" 的 LCS 是 "BCAB"(长4)。

若 X[i] == Y[j]:  dp[i][j] = dp[i-1][j-1] + 1     (末字符相同,接上)
否则:             dp[i][j] = max(dp[i-1][j], dp[i][j-1])  (去掉一个末字符取较优)

6.3.7 其它经典 DP

6.3.8 分治 vs 动态规划(必考辨析)

分治 动态规划
子问题 相互独立(不重叠) 重叠(反复出现)
是否存子问题解 不存(本就不重复算) 存(记忆化),避免重算
方向 自顶向下递归 通常自底向上
典型 归并/快排/二分 背包/LCS/斐波那契/Floyd

6.4 回溯法(Backtracking)

6.4.1 核心思想

回溯是一种系统穷举所有可能解的方法,用于「从一堆候选里找出满足条件的组合/排列」。它把解的构造过程想象成一棵解空间树:从根开始,一步步做选择往下走(深度优先),每走一步检查是否还可能通向合法解——若发现此路不通(违反约束),就「回退」到上一步换另一个选择。这个「退回去换路」就是「回溯」。

它本质是带剪枝的 DFS:在解空间树上深度优先搜索,靠「约束条件」提前砍掉不可能成功的分支(剪枝),避免盲目穷举,比纯暴力快。

6.4.2 通用框架

回溯(当前层/状态):
    if (到达一个完整解):
        记录/输出解
        return
    for (每一个可能的下一步选择):
        if (这个选择满足约束):        // 剪枝:不满足就跳过
            做出选择(修改状态)
            回溯(下一层/新状态)        // 递归深入
            撤销选择(恢复状态)        // 关键!回溯——退回来,试下一个

那句「撤销选择」是回溯的灵魂:试完一条路要把状态恢复原样,才能干净地试下一条。

6.4.3 经典例子:N 皇后完整推演

在 N×N 棋盘放 N 个皇后,使其两两不在同一行、列、斜线上。

思路:逐行放皇后(每行必放一个,自动保证不同行)。第 r 行尝试每一列 c:检查 (r,c) 是否和前面已放的皇后冲突(同列?同斜线?)。不冲突就放下,递归到第 r+1 行;若整行都放不下,回退到第 r−1 行换列。

4 皇后的推演(行从0开始):

第0行放(0,0)。
第1行:(1,0)同列✗ (1,1)斜线✗ (1,2)✓ → 放(1,2)。
第2行:(2,0)同列(0,0)✗ (2,1)斜线(1,2)✗ (2,2)同列✗ (2,3)斜线✗ → 整行都不行!
回溯到第1行:(1,2)撤销,试(1,3)✓ → 放(1,3)。
第2行:(2,0)✗ (2,1)✓ → 放(2,1)。
第3行:(3,0)✗(同列0,0) (3,1)✗ (3,2)✗(斜线2,1) (3,3)✗ → 整行不行!
回溯第2行:(2,1)撤销,(2,2)✗ (2,3)✗ → 第2行没别的,回溯第1行。
第1行:(1,3)撤销,没别的列了,回溯第0行。
第0行:(0,0)撤销,试(0,1) → 放(0,1)。
... 继续,最终找到解:(0,1)(1,3)(2,0)(3,2)。

这个「试→冲突→退回换列」的过程就是回溯。N 皇后是回溯的招牌题。

6.4.4 其它经典例子与考点

6.5 分支限界法(了解,按大纲)

分支限界和回溯都在「解空间树」上搜索,但策略不同:

一句话区分:回溯 = DFS 找所有解;分支限界 = BFS/优先级找最优解。 了解这个区别即可。

6.6 四大策略对比总览(高频辨析)

策略 核心思想 关键特征 经典例子
分治 分解成独立子问题,递归解,合并 子问题独立 归并/快排/二分/快速幂
贪心 每步取当前最优,不回头 局部最优→全局最优(需性质) 哈夫曼/Prim/Kruskal/Dijkstra/分数背包
动态规划 存储重叠子问题的解,自底向上 子问题重叠+最优子结构 0-1背包/LCS/斐波那契/Floyd
回溯 深度优先试探+剪枝+回退 系统穷举解空间树 N皇后/全排列/迷宫
分支限界 广度/优先级搜索+界限剪枝 求最优解 0-1背包最优/TSP

最常考辨析

6.7 算法策略自测

  1. 分治三步骤?要求子问题什么关系?举三个分治例子。主定理大概解决什么?
  2. 贪心核心思想?一定最优吗?举三个前面学过的贪心算法。
  3. 为什么分数背包能贪心、0-1 背包不能?(举反例)
  4. DP 解决什么问题?和分治的关键区别?两个适用条件?解题四步?
  5. 用斐波那契说明「重叠子问题+记忆化」怎么把 O(2ⁿ) 降到 O(n)。
  6. 写出 0-1 背包的状态定义和转移方程,并用一组小数据填出 dp 表。
  7. 写出 LCS 的转移方程。
  8. 回溯核心思想?为什么是「带剪枝的 DFS」?「撤销选择」为什么关键?手推 4 皇后的前几步。
  9. 回溯和分支限界的区别?
  10. 归类:哈夫曼、Prim、Kruskal、Dijkstra、归并、快排、二分查找、Floyd、0-1背包 各属于哪种策略?

6.8 本段总自测清单

查找

排序

算法设计策略


6.9 全课总览:九章一张图

九章全部走完。最后把整门课收进一张图,建议你能凭记忆复述——能复述,就说明真正「烂熟于心」了:

                       数据结构与算法
                             │
   ┌─────────────────────────┼─────────────────────────┐
   │ 基础(第1章)               │ 具体结构                  │ 思想(第6章)
   │ ·逻辑结构 vs 存储结构       │                          │ ·分治(独立子问题)
   │ ·复杂度大O                │  线性(1对1)               │ ·贪心(局部最优)
   │ ·算法五大特性             │   ·线性表(顺序/链式)       │ ·动态规划(重叠子问题)
   │ ·权衡:时间vs空间          │   ·栈(LIFO)/队列(FIFO)    │ ·回溯(DFS+剪枝)
   └─────────────────────────┤   ·串(KMP)、数组/矩阵      │ ·分支限界(求最优)
                             │  树(1对多)                 └─────────
                             │   ·二叉树/三种遍历/还原
                             │   ·BST/AVL/哈夫曼/堆
                             │   ·并查集
                             │  图(多对多)
                             │   ·邻接矩阵/表
                             │   ·DFS/BFS
                             │   ·MST(Prim/Kruskal)
                             │   ·最短路(Dijkstra/Floyd)
                             │   ·拓扑排序/关键路径
                             │
                             │ 操作(把结构拿来用)
                             │   ·查找(顺序/二分/分块/树/哈希)
                             │   ·排序(插入/交换/选择/归并/分配/外部)
                             └─────────────────────────

剧情主线再回顾:逻辑结构由简到繁(线性→树→图);每种结构都有顺序和链式两种存储、都问「怎么存/查/增删/遍历」四个问题;查找和排序是把结构拿来干活;算法设计策略是跳出具体结构的通用思想。 全课灵魂始终是一句话——没有最好的结构,只有最合适的;每个设计都是在「时间 vs 空间」「访问快 vs 增删快」之间做权衡。


三段全部写完。 接下来是你的 3 天刷题巩固期。收尾建议:

  1. 把三个文件的「自测清单」全部过一遍,能凭记忆复述的打勾,卡壳的回去重读对应小节。
  2. 手算题清单(「会就送分」,优先练到闭眼会):KMP 求 next、二叉树由前序中序还原、BST 删除、AVL 四种旋转、哈夫曼构造+WPL、Dijkstra 填表、Prim/Kruskal、拓扑排序、二分判定树 ASL、哈希表 ASL(两种)、各排序算法一趟/多趟模拟、0-1 背包 DP 填表。
  3. 代码默写:把每个结构的核心代码(链表增删、循环队列、二叉树遍历、BST、快排、归并、堆排、KMP)盖住自己敲一遍——这是「精通代码实现」的关键。
  4. 对照课本目录查漏:中科大课本若对某些点(红黑树、B+树细节、外部排序深度、分支限界等)有特别侧重或额外内容,以课本为准补上。

任何一章想要更深、更多例题,或针对某难点(AVL 旋转、DP 转移方程、KMP next 推导、哈希 ASL)单独展开,随时说。加油 💪