算法题分类:从题目特征到解题模型
本文最后更新于 2026年8月9日
LOCAL NOTE · REWRITTEN & SANITIZED
刷题最难的部分通常不是把模板敲出来,而是判断题目属于什么模型。分类的目标也不是给每道题贴唯一标签,而是快速发现它利用了哪种结构:连续性、单调性、重复状态、局部最优或图上的连通关系。
四个诊断问题
读完题目后,我会先问四件事:
- 搜索空间是什么:数组区间、树、图、排列组合,还是状态序列?
- 目标是什么:存在性、计数、最值、路径,还是枚举全部方案?
- 有没有可利用结构:有序、单调、连续、局部依赖或重叠子问题?
- 状态如何变化:加入一个元素能否增量更新,选择一次后能否撤销?
flowchart TD
A[读取题目] --> B{连续子数组或子串}
B -->|是| C[滑动窗口 / 前缀和]
B -->|否| D{判定函数单调}
D -->|是| E[二分答案]
D -->|否| F{需要枚举所有方案}
F -->|是| G[回溯 + 剪枝]
F -->|否| H{存在重叠子问题}
H -->|是| I[动态规划]
H -->|否| J{节点与边关系}
J -->|是| K[BFS / DFS / 最短路]
J -->|否| L[哈希 / 堆 / 栈 / 贪心]
题目特征速查
| 题目中的信号 | 优先想到 | 核心问题 |
|---|---|---|
| 判断元素是否出现、频次统计 | 哈希表 | 键和值分别表示什么 |
| 连续区间和、多次区间查询 | 前缀和 | 是否需要一维或二维前缀 |
| 最长或最短连续子串 | 滑动窗口 | 何时扩张、收缩、更新答案 |
| 第一个或最后一个满足条件的位置 | 二分查找 | 判定函数是否单调 |
| Top-K、动态最值 | 堆 | 维护大顶堆还是小顶堆 |
| 左右第一个更大或更小元素 | 单调栈 | 栈内保持递增还是递减 |
| 无权图最少步数 | BFS | 状态去重和层数如何记录 |
| 枚举组合、排列、切割方案 | 回溯 | 路径、选择列表、结束条件 |
| 最值且存在重复子问题 | 动态规划 | 状态、选择、转移方程 |
| 区间合并或覆盖 | 排序 + 贪心 | 按起点还是终点排序 |
递归的三种面貌
递归只是一种实现形式,背后至少有三种不同思维。
遍历
遍历把递归函数看作进入每个节点的过程,通常通过外部变量收集结果。树的 DFS、图遍历和部分回溯属于这一类。
分解问题
分解问题要求先得到子问题的返回值,再合成当前问题。例如二叉树最大深度:当前深度等于左右子树最大深度加一。归并排序和树形 DP 也属于这条路线。
枚举决策树
回溯是在决策树上进行 DFS。每个节点包含三项:已经选择的路径、剩余选择列表、结束条件。
1 | |
“做选择、递归、撤销选择”必须对称。剪枝一般放在递归调用前,因为目标就是避免进入不可能产生答案的子树。
回溯与动态规划的分界
两者都可能来自暴力穷举,但优化方式不同:
- 回溯关注枚举不同决策路径,通常需要全部方案或约束下的可行方案;
- 动态规划关注多个路径是否到达同一状态,通过记忆化避免重复计算;
- 若问题要求列出所有排列,重复状态通常不能直接合并;
- 若问题只求最值或计数,并且未来只依赖有限状态,往往可以设计 DP。
动态规划可以按以下顺序建模:
- 定义
dp状态及其语义; - 枚举当前状态允许的选择;
- 写出状态转移;
- 根据状态定义确定 base case;
- 确定遍历顺序,保证依赖状态已计算;
- 用小样例手推数组,检查边界和非法状态。
数据结构不是题型,维护目标才是
同一道题可能出现多种数据结构,关键是它们维护了什么不变量:
- 哈希表维护“是否出现”或“出现次数”;
- 堆维护动态集合中的极值;
- 单调栈维护尚未找到答案的一组候选元素;
- 并查集维护动态连通分量;
- Trie 维护字符串前缀;
- 双端队列可以维护滑动窗口中的单调候选集。
因此看到“使用堆”并不算分析完成,还要说明堆顶代表什么、元素何时入堆和出堆,以及堆的大小是否有界。
建立自己的错题索引
相比按题号记录,我更倾向于按“错误原因”归档:
- 没识别出单调性;
- 搜索区间定义前后不一致;
- 状态定义缺少一个维度;
- 回溯没有正确撤销状态;
- BFS 忘记在入队时去重;
- 贪心选择缺少交换论证;
- 只记住模板,没有证明使用条件。
这样复习时看到的不是几十份互不相关的答案,而是一组会反复出现的思维缺口。后续专题会继续拆分图论、动态规划、回溯和单调数据结构。
算法题分类:从题目特征到解题模型
https://linshenggithub.github.io/notes/2026/01/algorithm-problem-taxonomy/