蓝桥杯 C++ 组算法知识点整理·
写在前面
这篇文章我没有按"只适合考前半小时翻"的速查风格去写,而是按"可以长期收藏、反复回看、边学边练"的长文去整理。读者大大可以把它理解成一份围绕蓝桥杯 C++ 组的完整知识手册:前面讲基础和题型,中间讲核心算法,后面讲训练路线、真题策略和模板压缩。
目录
- 写在前面
- [一、C++ 基础与 STL](#一、C++ 基础与 STL)
- 二、枚举、模拟、排序、二分与技巧
- 三、前缀和、差分、双指针、位运算与离散化
- 四、搜索专题:DFS、BFS、回溯与剪枝
- 五、动态规划专题
- 六、图论与常用数据结构
- 七、字符串专题
- 八、数论与组合数学
- 九、真题题型路线与备赛策略
- 十、模板速查手册
- 文末结语
这篇文章适合谁看
- 正在准备蓝桥杯 C++ 组,想系统过一遍知识点的人。
- 平时刷过一些题,但专题之间没有串起来的人。
正文
一、C++ 基础与 STL
本章适合谁
- 刚开始准备蓝桥杯,担心语言基础不稳的人。
- 做题经常因为
sort、vector、下标、类型写错而丢分的人。 - 明明知道算法思路,却总在实现阶段卡壳的人。
建议前置知识
- 会写最基本的
for、if、函数。 - 知道数组、字符串、结构体的基本概念。
- 能看懂简单的 C++ 代码。
本章你要掌握什么
| 板块 | 目标 |
|---|---|
| 竞赛模板 | 能在 1 分钟内写出一个干净的比赛开局模板 |
| 输入输出 | 知道什么时候该加速,什么时候要注意格式 |
| 类型选择 | 知道哪些量必须用 long long |
| STL 容器 | 会选容器,不乱用容器 |
| 常用算法 | 排序、二分、去重、全排列这些必须顺手 |
| 语言陷阱 | 尽量不因为实现细节吃亏 |
知识图谱 / 题型雷达
| 看到什么现象 | 优先想到什么 |
|---|---|
| 需要维护一个动态数组 | vector |
| 需要先进先出 | queue |
| 需要后进先出 | stack |
| 需要自动按序维护且去重 | set |
| 需要统计映射关系 | map |
| 需要维护当前最大值 / 最小值 | priority_queue |
| 需要排序再处理 | sort + 自定义比较器 |
| 需要有序查找第一个满足条件的位置 | lower_bound / upper_bound |
竞赛常用模板
最基础的比赛模板
什么时候用:
- 开考刚建文件时。
- 任何普通算法题的起手式。
核心思路:
- 头文件、类型别名、常量、输入输出加速就绪。
- 模板要短,不能影响你开题速度。
模板:
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3fLL;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
return 0;
}
复杂度:
- 模板本身不涉及算法复杂度。
典型题型:
- 几乎所有普通题都从这个模板开始。
易错点:
long long常量要带LL。- 别在比赛里不断切换模板风格。
推荐练习:
- 连续手敲 5 次这个模板,要求不看资料也能写对。
带全局数组的模板
什么时候用:
- 题目需要较大数组。
- 搜索、图论、 DP 需要共享全局状态。
核心思路:
- 大数组尽量放全局,避免局部爆栈。
模板:
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int a[N];
ll s[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
return 0;
}
复杂度:
- 由后续算法决定。
典型题型:
- 前缀和、差分、图、搜索、 DP。
易错点:
N估小了最致命。- 一维够用时不要乱开二维。
推荐练习:
- 用全局数组写 3 道需要
1e5规模输入的题。
输入输出与数据范围
什么时候需要加速
什么时候用:
- 输入量大、数据多组、字符串或图边很多时。
核心思路:
cin/cout默认同步 C 的输入输出,速度较慢。- 关闭同步并解绑
cin和cout后,竞赛里一般够用。
模板:
cpp
ios::sync_with_stdio(false);
cin.tie(nullptr);
复杂度:
- 这是常数级优化。
典型题型:
- 大数据读入题、图论、批量询问、真题模拟。
易错点:
- 已经用了
scanf/printf时不要乱和cin/cout混用。
推荐练习:
- 找一道大输入题,分别测一下加速前后的体验。
int 还是 long long
什么时候用:
- 任何题都必须先做这个判断。
核心思路:
- 只要答案或中间量可能超过
2e9,就要高度警惕。
经验判断表:
| 情况 | 推荐类型 |
|---|---|
| 数组下标、循环变量 | int |
| 前缀和、区间和 | long long |
两个 1e9 量级的乘法 |
long long 起步 |
| 方案数、路径数 | 通常 long long |
| 图边权和最短路距离 | 常用 long long |
模板:
cpp
int n;
long long ans = 0;
vector<long long> a(n + 1);
复杂度:
- 类型本身不改变复杂度,但会影响正确性。
典型题型:
- 数论、前缀和、 DP、图论。
易错点:
int * int先溢出后再赋给long long也没用。- 正确写法往往是
1LL * a * b。
推荐练习:
- 把你之前做错过的一道溢出题重写一遍。
数组、字符串与下标习惯
下标风格统一
什么时候用:
- 写任何数组题之前。
核心思路:
0下标和1下标都能写,但必须从头到尾统一。
模板:
cpp
// 0 下标
for (int i = 0; i < n; i++) cin >> a[i];
// 1 下标
for (int i = 1; i <= n; i++) cin >> a[i];
复杂度:
- 无。
典型题型:
- 所有数组题。
易错点:
sort(a + 1, a + n + 1)和sort(a, a + n)混写。- 前缀和写成
s[l - 1]时,若l = 0就直接错了。
推荐练习:
- 专门手敲一遍
0下标和1下标的排序、前缀和模板。
string 与字符处理
什么时候用:
- 字符串题、模拟题、读入单词或整行文本。
核心思路:
- C++ 里优先用
string。 - 只在特定性能或模板场景下用字符数组。
模板:
cpp
string s;
cin >> s;
for (char c : s) {
// 逐字符处理
}
复杂度:
- 基本访问是
O(1),遍历是O(n)。
典型题型:
- 字符串匹配、模拟、括号题、回文串。
易错点:
getline前若有残留换行,需要先处理缓冲区。
推荐练习:
- 写一个统计大小写字母与数字数量的小程序。
常用算法函数
sort
什么时候用:
- 任何"先排序再处理"的题。
核心思路:
- 排序是竞赛里的万能预处理之一。
模板:
cpp
sort(a + 1, a + n + 1);
sort(v.begin(), v.end());
复杂度:
O(nlogn)。
典型题型:
- 贪心、二分、双指针、区间问题。
易错点:
- 区间写错最常见。
推荐练习:
- 写 3 种排序:数组、
vector、结构体。
自定义比较器
什么时候用:
- 排序规则不是简单升序。
核心思路:
- 比较器要体现"谁该排前面"。
模板:
cpp
struct Node {
int a, b;
};
bool cmp(const Node& x, const Node& y) {
if (x.a != y.a) return x.a < y.a;
return x.b > y.b;
}
sort(v.begin(), v.end(), cmp);
也可以写成 lambda:
cpp
sort(v.begin(), v.end(), [](const Node& x, const Node& y) {
if (x.a != y.a) return x.a < y.a;
return x.b > y.b;
});
复杂度:
O(nlogn)。
典型题型:
- 区间排序、成绩排序、二维关键字排序。
易错点:
- 多关键字顺序写反。
推荐练习:
- 写一个"先按第一关键字升序,再按第二关键字降序"的排序题。
reverse、 unique
什么时候用:
- 需要反转序列或排序后去重。
核心思路:
unique只会把相邻重复元素移到后面,常与排序联用。
模板:
cpp
reverse(v.begin(), v.end());
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
复杂度:
reverse是O(n)。unique是O(n),常配合排序总体O(nlogn)。
典型题型:
- 离散化、去重、种类统计。
易错点:
- 不排序直接
unique只去连续重复。
推荐练习:
- 写离散化模板时顺手练
sort + unique。
lower_bound 与 upper_bound
什么时候用:
- 有序区间里二分找位置。
核心思路:
lower_bound找第一个大于等于目标值的位置。upper_bound找第一个大于目标值的位置。
模板:
cpp
int pos1 = lower_bound(a + 1, a + n + 1, x) - a;
int pos2 = upper_bound(a + 1, a + n + 1, x) - a;
复杂度:
O(logn)。
典型题型:
- 查找第一个满足条件的位置。
- 统计某值出现次数。
易错点:
- 区间必须有序。
- 返回的是位置,不是值本身。
推荐练习:
- 写一个统计某数出现次数的程序:
upper_bound - lower_bound。
next_permutation
什么时候用:
- 枚举全排列。
核心思路:
- 先排序到最小字典序,再不断求下一个排列。
模板:
cpp
sort(v.begin(), v.end());
do {
// 使用当前排列
} while (next_permutation(v.begin(), v.end()));
复杂度:
- 全排列总体通常是
O(n * n!)。
典型题型:
- 小规模排列枚举、暴力构造。
易错点:
- 不先排序,枚举顺序和完整性都可能出问题。
推荐练习:
- 用
next_permutation输出1~4的所有排列。
std::gcd
什么时候用:
- 求最大公约数。
核心思路:
- 现代 C++ 推荐
std::gcd,头文件<numeric>。
模板:
cpp
#include <numeric>
int g = std::gcd(a, b);
复杂度:
- 约为
O(logn)。
典型题型:
- 数论、分数约分、整除关系。
易错点:
- 求
lcm时先乘后除容易溢出。
推荐练习:
- 写
gcd+lcm小模板。
常用 STL 容器
pair
什么时候用:
- 要把两个值打包一起传递。
核心思路:
- 最适合坐标、边、数对、状态。
模板:
cpp
pair<int, int> p = {3, 5};
cout << p.first << ' ' << p.second << '\n';
复杂度:
- 访问是
O(1)。
典型题型:
- BFS 坐标、区间端点、排序键值对。
易错点:
.first和.second写反。
推荐练习:
- 用
pair<int,int>存点坐标并排序。
vector
什么时候用:
- 动态数组场景。
核心思路:
- 绝大多数情况下,它比手写动态数组更方便。
模板:
cpp
vector<int> v;
v.push_back(3);
v.push_back(1);
v.push_back(2);
sort(v.begin(), v.end());
复杂度:
- 尾部插入均摊
O(1)。 - 随机访问
O(1)。
典型题型:
- 几乎所有动态存储题。
易错点:
- 下标访问前要保证非空。
erase中间元素是线性复杂度。
推荐练习:
- 用
vector写一个离散化小模板。
queue
什么时候用:
- BFS 或先进先出。
核心思路:
- 从队尾进,从队首出。
模板:
cpp
queue<int> q;
q.push(1);
q.push(2);
cout << q.front() << '\n';
q.pop();
复杂度:
- 入队出队一般是
O(1)。
典型题型:
- 网格 BFS、层序遍历、最少步数。
易错点:
queue没有clear()。
推荐练习:
- 写一个最短步数 BFS 模板。
stack
什么时候用:
- 后进先出、括号匹配、单调栈基础。
核心思路:
- 栈顶进栈顶出。
模板:
cpp
stack<int> st;
st.push(1);
st.push(2);
cout << st.top() << '\n';
st.pop();
复杂度:
- 常用操作
O(1)。
典型题型:
- 括号匹配、表达式处理、单调栈。
易错点:
- 空栈访问
top()会出错。
推荐练习:
- 写括号匹配基础题。
set 与 multiset
什么时候用:
- 需要有序去重,或者需要保留重复但维护有序。
核心思路:
set自动去重。multiset允许重复。
模板:
cpp
set<int> s;
s.insert(3);
s.insert(1);
s.insert(3);
bool ok = s.count(1);
复杂度:
- 插入、删除、查找通常
O(logn)。
典型题型:
- 动态判重、维护有序集合。
易错点:
set不是下标数组,不支持随机访问。
推荐练习:
- 写一个动态去重 + 输出升序结果的小程序。
map 与 unordered_map
什么时候用:
- 需要键值映射。
核心思路:
map有序,unordered_map平均更快但无序。
模板:
cpp
map<string, int> mp;
mp["lanqiao"]++;
mp["cup"] += 2;
复杂度:
map通常O(logn)。unordered_map平均O(1)。
典型题型:
- 计数、离散映射、哈希表模拟。
易错点:
mp[key]会在键不存在时自动插入。
推荐练习:
- 统计单词出现次数。
priority_queue
什么时候用:
- 需要反复取最大值或最小值。
核心思路:
- 默认大根堆。
- 小根堆要显式写比较器。
模板:
cpp
priority_queue<int> maxHeap;
priority_queue<int, vector<int>, greater<int>> minHeap;
复杂度:
- 插入、弹出堆顶通常
O(logn)。
典型题型:
- 堆、贪心、 Dijkstra。
易错点:
- 默认不是小根堆。
推荐练习:
- 写一个持续输出前 k 大值的小程序。
排序与二分常见组合技
排序后双指针
什么时候用:
- 两数和、区间压缩、配对问题。
核心思路:
- 排序让原本无序的问题变得可扫描。
模板:
cpp
sort(a.begin(), a.end());
int l = 0, r = (int)a.size() - 1;
while (l < r) {
long long sum = 1LL * a[l] + a[r];
if (sum == target) break;
if (sum < target) l++;
else r--;
}
复杂度:
- 排序
O(nlogn),扫描O(n)。
典型题型:
- 两数和、最接近目标值、配对问题。
易错点:
- 忘了排序。
推荐练习:
- 有序数组找两数和。
排序后去重再离散化
什么时候用:
- 值域大,但实际出现值少。
核心思路:
sort + unique + lower_bound是离散化基础三件套。
模板:
cpp
vector<int> alls = a;
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
int id = lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
复杂度:
O(nlogn)。
典型题型:
- 坐标压缩、树状数组、线段树预处理。
易错点:
- 编号从
0还是1开始要统一。
推荐练习:
- 用离散化改写一个值域很大的前缀统计题。
常见语言陷阱
memset 不是万能初始化
什么时候用:
- 想快速初始化数组时。
核心思路:
memset安全用在0和-1最常见。
模板:
cpp
memset(vis, 0, sizeof vis);
memset(dist, -1, sizeof dist);
复杂度:
- 线性。
典型题型:
- BFS 访问数组、初始化整型数组。
易错点:
- 对
long long数组设成1e18不能用memset。
推荐练习:
- 用
fill改写一个大数组初始化。
引用、拷贝与范围 for
什么时候用:
- 遍历容器时。
核心思路:
- 只读时用值或
const auto&。 - 需要修改元素时用引用。
模板:
cpp
for (auto x : v) {
// 读副本
}
for (auto& x : v) {
x += 1;
}
for (const auto& x : v) {
cout << x << '\n';
}
复杂度:
- 遍历本身线性。
典型题型:
- 容器处理题。
易错点:
- 忘了
&,改的是副本不是原值。
推荐练习:
- 写一个把
vector所有元素加一的测试程序。
本章常见题型识别
| 题目现象 | 优先工具 |
|---|---|
| 数据量不大,但实现细节多 | string / vector / 模拟 |
| 需要排序后再处理 | sort / 比较器 |
| 需要去重 | sort + unique / set |
| 需要查某个值第一次出现的位置 | lower_bound |
| 需要不断拿到当前最小值 | 小根堆 |
| 需要统计频率 | map / 数组计数 |
本章易错点总表
| 错误类型 | 典型表现 | 怎么避免 |
|---|---|---|
| 区间写错 | sort(a + 1, a + n) |
记住右端点是开区间 |
| 类型溢出 | int ans += a[i] * b[i] |
提前写 1LL * |
| 空容器访问 | v.back()、q.front() |
先判空 |
| 去重失效 | 不排序直接 unique |
先排后去 |
| 堆方向写反 | 想要最小值却用默认堆 | 显式写 greater<int> |
| 多组数据污染 | vector、map 没清空 |
每组开始前初始化 |
本章练习路线
| 练习顺序 | 练习方向 | 目标能力 | 官方入口建议 |
|---|---|---|---|
| 1 | 数组读写与排序 | 熟悉比赛输入和排序 | 题库首页 搜索"排序" |
| 2 | 自定义排序 | 学会多关键字比较 | 题库首页 搜索"排序 + 结构体" |
| 3 | 去重与离散化 | 练 sort + unique |
题库首页 搜索"离散化" |
| 4 | STL 计数 | 学会 map / set |
题库首页 搜索"统计" |
| 5 | 堆与优先队列 | 学会维护最值 | 题库首页 搜索"优先队列" |
| 6 | 字符串模拟 | 熟悉 string |
题库首页 搜索"字符串" |
| 7 | 二分函数使用 | 学会 lower_bound |
题库首页 搜索"二分" |
| 8 | 真题热身 | 用语言基础稳拿分 | 蓝桥杯真题卷 选择 C/C++ 近年省赛 |
| 9 | 实现细节题 | 练细节与调试 | 历届真题课程 |
| 10 | 自我手敲测试 | 不看资料手写模板 | 回到本章与附录 |
STL 选型对照
| 场景 | 更推荐的容器 |
|---|---|
| 动态数组 | vector |
| 先进先出 | queue |
| 后进先出 | stack |
| 自动去重且有序 | set |
| 计数 / 映射 | map |
| 频繁拿最大 / 最小 | priority_queue |
| 简单二元状态 | pair |
本章自测问题
-
- 我能不看资料写比赛基础模板吗?
-
- 我知道
sort的区间写法吗?
- 我知道
-
- 我知道
lower_bound和upper_bound的区别吗?
- 我知道
-
- 我知道什么时候该用
vector吗?
- 我知道什么时候该用
-
- 我知道什么时候该用
set吗?
- 我知道什么时候该用
-
- 我知道
priority_queue默认是大根堆吗?
- 我知道
-
- 我知道
map[key]会自动插入吗?
- 我知道
-
- 我知道
unique后为什么还要erase吗?
- 我知道
-
- 我知道什么时候必须用
long long吗?
- 我知道什么时候必须用
-
- 我已经形成固定的下标风格了吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 这一章看完后,你至少要把排序、二分函数、去重、
vector、map、堆写顺手。 - 如果你现在还经常在 STL 上卡住,不建议急着冲更难专题。
- 最好的检验方法不是"看懂了",而是"不看文档手敲 10 分钟"。
二、枚举、模拟、排序、二分与技巧
本章适合谁
- 会一点基础语法和 STL,但做题总觉得没有"题感"的同学。
- 看到题目不知道该暴力、该二分、还是该模拟的人。
- 经常把简单题写复杂,把中档题写崩的人。
建议前置知识
- 已掌握第 1 章内容。
- 会写基本循环和排序。
本章目标
| 板块 | 你需要达到的程度 |
|---|---|
| 枚举 | 知道什么时候暴力能过,什么时候必须优化 |
| 模拟 | 能把题目要求翻译成程序流程 |
| 排序 | 知道很多题先排一下就会好做很多 |
| 二分 | 会写基础二分、二分答案、边界判断 |
| 复杂度判断 | 能在动手前先估计是否会超时 |
| 实现技巧 | 能减少 WA 和 TLE |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 数据规模小、要求列举所有情况 | 枚举 |
| 题意规则多但每一步可直接按要求执行 | 模拟 |
| 要求先最小化 / 最大化某种代价 | 排序 + 贪心 或 二分答案 |
| 答案满足单调性 | 二分答案 |
| 给定有序数组查目标 | 二分查找 |
| 写法多但复杂度关键 | 先估 n、nlogn、n^2 是否可过 |
枚举
什么叫枚举
什么时候用:
- 数据范围小。
- 方案空间可以直接遍历。
- 暴力虽朴素,但足够稳定。
核心思路:
- 不要一看到暴力就害怕。
- 关键是先估复杂度,而不是先入为主觉得"暴力不高级"。
模板:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
// 判断 i, j 这组状态是否合法
}
}
复杂度:
- 单层
O(n),双层O(n^2),三层O(n^3)。
典型题型:
- 小范围数对统计。
- 小规模搜索前的朴素做法。
易错点:
- 不看
n就乱写三重循环。
推荐练习:
- 写一个统计满足条件数对 / 三元组的小程序,并手算复杂度。
枚举的优化思路
什么时候用:
- 暴力快超时,但问题仍保留枚举主线。
核心思路:
- 缩减枚举范围。
- 预处理一部分信息。
- 把多层枚举压成一层或两层。
模板:
cpp
sort(a.begin(), a.end());
for (int i = 0; i < n; i++) {
int pos = lower_bound(a.begin() + i + 1, a.end(), target - a[i]) - a.begin();
// 用 pos 做后续处理
}
复杂度:
- 常见从
O(n^2)优化到O(nlogn)。
典型题型:
- 数对统计、区间条件判断。
易错点:
- 优化后边界更复杂。
推荐练习:
- 把一个双层暴力题优化到
O(nlogn)。
模拟
模拟的本质
什么时候用:
- 题目规则清晰,每一步都能按文字描述执行。
核心思路:
- 模拟题先别急着写代码,先把流程按人类步骤列出来。
做题流程:
- 读题,抽取状态量。
- 确定每一步的更新顺序。
- 列清楚输入、状态、输出。
- 再动手写。
模板:
cpp
for (int step = 1; step <= m; step++) {
// 读取本轮操作
// 判断类型
// 更新状态
}
复杂度:
- 常依赖操作次数和每步更新代价。
典型题型:
- 日程安排、棋盘移动、字符串操作、数字变换。
易错点:
- 状态更新顺序不对。
- 漏掉边界规则。
推荐练习:
- 找一道带多个操作类型的模拟题,先写伪代码再实现。
模拟题的检查清单
什么时候用:
- 写完模拟题准备提交前。
核心思路:
- 模拟题经常不是"不会",而是"漏条件"。
检查表:
- 初始状态是否正确。
- 每一步更新是否按题意顺序。
- 特殊输入是否处理。
- 结束条件是否写对。
- 样例之外的边界是否考虑。
典型题型:
- 多操作系统模拟。
易错点:
- 忘记清空容器。
推荐练习:
- 把自己的模拟错题按"漏条件 / 顺序错 / 边界错"分类。
排序思想
很多题为什么先排序
什么时候用:
- 题目涉及大小关系、配对、最值、相邻关系。
核心思路:
- 排序能把杂乱输入变成有结构序列。
- 一旦有序,二分、双指针、贪心都更容易使用。
模板:
cpp
sort(a.begin(), a.end());
复杂度:
O(nlogn)。
典型题型:
- 区间覆盖、配对、贪心、离散化。
易错点:
- 排完序后忘记原下标。
推荐练习:
- 做一题"不排序难写,排序后很自然"的题。
排序后常见处理模式
什么时候用:
- 一旦你已经排序完成。
核心思路:
- 排完序后,常见套路有 4 类:顺序扫描、相邻比较、双指针、二分边界。
模板:
cpp
sort(a.begin(), a.end());
for (int i = 1; i < n; i++) {
// 比较 a[i] 和 a[i - 1]
}
复杂度:
- 排序后扫描通常是线性。
典型题型:
- 求相邻差最小值、去重、合并段。
易错点:
- 循环起点和终点错一位。
推荐练习:
- 练一题相邻元素处理题。
二分查找
有序数组中的基础二分
什么时候用:
- 已知数组有序,要找某个值是否存在,或者找某个边界。
核心思路:
- 每次取中点,利用有序性砍掉一半区间。
模板:
cpp
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] == target) {
break;
} else if (a[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
复杂度:
O(logn)。
典型题型:
- 查值、查位置、找第一个 / 最后一个满足条件的位置。
易错点:
mid计算写成(l + r) / 2在大范围时可能溢出。
推荐练习:
- 手写一个不依赖
lower_bound的二分查找模板。
找左边界 / 右边界
什么时候用:
- 需要找第一个满足条件的位置或最后一个满足条件的位置。
核心思路:
- 左边界和右边界的写法不同,核心是保持单调性和不变量。
左边界模板:
cpp
int l = 0, r = n - 1, ans = n;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] >= target) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
右边界模板:
cpp
int l = 0, r = n - 1, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] <= target) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
复杂度:
O(logn)。
典型题型:
- 统计某值出现次数。
易错点:
- 判断条件和更新方向没配套。
推荐练习:
- 自己实现
lower_bound和upper_bound。
二分答案
什么题可以二分答案
什么时候用:
- 题目要求最小值 / 最大值。
- 给定一个答案后,可以快速判断"是否可行"。
- 可行性随着答案变化呈单调性。
核心思路:
- 二分的不是数据,而是"答案空间"。
- 核心在
check(mid)。
模板:
cpp
bool check(long long mid) {
// 判断答案取 mid 时是否可行
}
long long l = 0, r = 1e18, ans = -1;
while (l <= r) {
long long mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
复杂度:
O(log答案范围 * check复杂度)。
典型题型:
- 最小最大值、最少时间、最大可行长度。
易错点:
- 根本没有单调性却硬二分。
check函数写错,二分再标准也没用。
推荐练习:
- 做两道典型"最小化最大值"题。
二分答案的识别信号
什么时候用:
- 读题阶段。
核心思路:
- 一看到这些关键词就应该警觉。
| 关键词 | 是否考虑二分答案 |
|---|---|
| 最小的最大值 | 高度考虑 |
| 最大的最小值 | 高度考虑 |
| 至少 / 至多 / 不超过 | 高度考虑 |
| 能否在 x 时间内完成 | 高度考虑 |
| 切分、装载、安排、覆盖 | 很多时候可二分 |
典型题型:
- 木材切割、机器生产、安排区间、装箱。
易错点:
- 题目可以贪心不代表不能二分,反之也一样。
推荐练习:
- 自己整理一份"能二分答案的题目特征"。
复杂度判断
一眼判断能不能过
什么时候用:
- 动手写代码之前。
核心思路:
- 先看数据范围,再决定做法。
经验表:
| 数据范围 | 常见可行复杂度 |
|---|---|
n <= 20 |
2^n、回溯、状态压缩 |
n <= 200 |
n^3 有时可过 |
n <= 2000 |
n^2 较稳 |
n <= 1e5 |
nlogn、线性 |
n <= 1e6 |
线性或接近线性 |
典型题型:
- 所有题。
易错点:
- 忽略常数和多组数据。
推荐练习:
- 给自己做一个"看到范围先估复杂度"的习惯训练。
复杂度常见误判
什么时候用:
- 提交前最后检查思路。
核心思路:
- 有些代码看着不复杂,实际上已经超时。
常见误判:
- 在循环里做
erase。 - 在
map上套很多重操作。 - 搜索里状态重复访问。
- 每次查询都重新排序。
复杂度:
- 很容易从
O(nlogn)误写成O(qnlogn)。
典型题型:
- 多次查询题、动态维护题。
易错点:
- 只盯单次操作,不看总次数。
推荐练习:
- 把自己做过的一个 TLE 题复盘成复杂度错误清单。
实现技巧
先写暴力,再想优化
什么时候用:
- 中档题卡思路时。
核心思路:
- 暴力解能帮助你确认状态和正确性。
- 很多优化就建立在暴力思路之上。
典型题型:
- 枚举优化、 DP、二分答案。
易错点:
- 直接追求高级做法,结果思路和实现都乱。
推荐练习:
- 先写暴力版,再写优化版,并比较两者差异。
伪代码先行
什么时候用:
- 模拟题、复杂实现题。
核心思路:
- 先写自然语言步骤,再翻译成代码。
示例:
text
1. 读入数据
2. 按某关键字排序
3. 枚举每个区间
4. 用 check 判定当前方案是否可行
5. 输出答案
典型题型:
- 模拟、复杂贪心、分情况处理题。
易错点:
- 没想清楚流程就直接写,越写越乱。
推荐练习:
- 每做一道中档题,先写 5 行伪代码。
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 小范围全部列举 | 枚举 / 回溯 |
| 操作规则固定 | 模拟 |
| 答案具有单调性 | 二分答案 |
| 需要配对或按序关系处理 | 排序 |
| 需要在有序序列找边界 | 二分 / lower_bound |
本章易错点总表
| 错误类型 | 具体表现 | 避免方式 |
|---|---|---|
| 二分死循环 | mid 和更新方向不匹配 |
固定模板,别临场发明 |
| 复杂度超限 | 忽略多组数据或内层操作 | 先估总复杂度 |
| 模拟漏条件 | 只看样例,不看文字细节 | 写检查清单 |
| 枚举重复计数 | 没控制顺序或范围 | 明确循环边界 |
| 二分答案失效 | check 无单调性 |
先证明单调再写 |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 简单枚举 | 建立复杂度意识 | 题库首页 搜索"枚举" |
| 2 | 多条件模拟 | 学会状态更新顺序 | 题库首页 搜索"模拟" |
| 3 | 排序后扫描 | 感受排序带来的结构化 | 题库首页 搜索"排序" |
| 4 | 基础二分 | 稳定写出左右边界 | 题库首页 搜索"二分" |
| 5 | 二分答案入门 | 训练 check 思维 |
题库首页 搜索"二分答案" |
| 6 | 枚举优化 | 学会从暴力转 nlogn |
题库首页 搜索"优化" |
| 7 | 排序 + 贪心题 | 学会观察关键字 | 蓝桥杯真题卷 选近年基础题 |
| 8 | 边界多的实现题 | 训练稳定性 | 历届真题课程 |
| 9 | 限时实现 | 20 分钟内写完一题 | 蓝桥杯真题卷 |
| 10 | 错题复盘 | 归纳"为什么没想到二分 / 排序" | 回看本章 |
二分答案判定表
| 题目说法 | 是否高度考虑二分答案 |
|---|---|
| 最小的最大值 | 是 |
| 最大的最小值 | 是 |
| 至少需要多久 | 是 |
| 最多可以做到多大 | 是 |
| 能否在 x 内完成 | 是 |
| 单纯统计所有方案 | 不一定 |
本章自测问题
-
- 我会先看数据范围再决定做法吗?
-
- 我能区分基础二分和二分答案吗?
-
- 我知道
check是二分答案的核心吗?
- 我知道
-
- 我会给模拟题先写伪代码吗?
-
- 我会在写枚举前先算复杂度吗?
-
- 我能看出一题为什么"先排序再处理"吗?
-
- 我会写左右边界二分吗?
-
- 我知道什么时候暴力就够用吗?
-
- 我会在实现复杂题前先想状态和流程吗?
-
- 我会把 TLE 复盘成复杂度问题吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 这一章最关键的不是"会背定义",而是学会在读题阶段做判断。
- 你要逐渐形成条件反射:规模小先枚举,规则多先模拟,有序性先二分,大小关系先排序。
- 如果你总在这里卡住,后面的搜索、 DP、图论就很难真正稳起来。
三、前缀和、差分、双指针、位运算与离散化
本章适合谁
- 想把蓝桥杯最高频模板真正吃透的人。
- 做区间题、连续子段题、数组扫描题经常没思路的人。
- 明明学过这些算法,但一到考场就想不起来怎么落代码的人。
建议前置知识
- 已掌握第 1 章和第 2 章。
- 能读懂数组与排序代码。
本章目标
| 板块 | 目标 |
|---|---|
| 前缀和 | 会做一维、二维区间求和 |
| 差分 | 会做批量区间加减 |
| 双指针 | 会写对撞、快慢、滑动窗口三种模式 |
| 位运算 | 能处理位判断、子集枚举、 lowbit |
| 离散化 | 知道值域大但实际值少时怎么处理 |
| 高频坑点 | 不在边界和下标上翻车 |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 多次区间求和 | 前缀和 |
| 多次区间修改,最后统一查询 | 差分 |
| 最长 / 最短连续区间 | 滑动窗口 |
| 有序数组找两数和 | 对撞指针 |
| 原地去重 / 压缩 | 快慢指针 |
| 需要处理二进制状态 | 位运算 |
| 值域很大但出现次数不多 | 离散化 |
前缀和
一维前缀和
什么时候用:
- 多次询问区间和。
- 想快速计算很多连续子段的和。
核心思路:
s[i]表示前i项之和。- 区间
l ~ r的和等于s[r] - s[l - 1]。
模板:
cpp
int n;
vector<long long> a(n + 1), s(n + 1, 0);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + a[i];
}
复杂度:
- 预处理
O(n),单次查询O(1)。
典型题型:
- 区间和。
- 连续子段和。
易错点:
s[0]忘记为0。- 和数组开成
int。
推荐练习:
- 练 3 道"区间和查询"题。
前缀和的变形用法
什么时候用:
- 不只是直接求和,而是把信息累起来。
核心思路:
- 前缀和本质是累积信息。
- 只要某种量支持"前缀累积 + 相减还原区间",就能类比使用。
常见变形:
| 变形 | 作用 |
|---|---|
| 前缀计数 | 统计区间内某元素个数 |
| 前缀奇偶 | 判断区间奇偶特征 |
| 前缀异或 | 处理异或区间信息 |
模板:
cpp
vector<int> cnt(n + 1, 0);
for (int i = 1; i <= n; i++) {
cnt[i] = cnt[i - 1] + (a[i] == target);
}
复杂度:
- 预处理
O(n)。
典型题型:
- 区间计数、区间奇偶。
易错点:
- 把"能前缀和"的量和"不能直接相减"的量混淆。
推荐练习:
- 做一道"区间中某数出现次数"的题。
二维前缀和
什么时候用:
- 矩阵子矩形求和。
- 二维网格统计。
核心思路:
sum[i][j]表示左上角到(i,j)的矩形和。- 子矩形和用容斥求。
模板:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
}
}
复杂度:
- 预处理
O(nm),单次查询O(1)。
典型题型:
- 子矩形和、棋盘统计。
易错点:
- 容斥公式符号写反。
推荐练习:
- 练一题二维区间和。
差分
一维差分
什么时候用:
- 多次区间加减,最后统一输出结果。
核心思路:
- 给区间
l ~ r加上c,只改diff[l] += c, diff[r + 1] -= c。
模板:
cpp
vector<long long> diff(n + 2, 0), a(n + 1, 0);
for (int i = 1; i <= m; i++) {
int l, r;
long long c;
cin >> l >> r >> c;
diff[l] += c;
diff[r + 1] -= c;
}
for (int i = 1; i <= n; i++) {
diff[i] += diff[i - 1];
a[i] += diff[i];
}
复杂度:
- 每次修改
O(1),最后恢复O(n)。
典型题型:
- 区间加法、覆盖统计。
易错点:
- 忘记多开一位。
- 初始数组非零时没叠加回去。
推荐练习:
- 写一道典型区间加法题。
二维差分
什么时候用:
- 多次对子矩形做批量加减。
核心思路:
- 类似一维差分,但要在四个角上打标记。
模板:
cpp
auto add = [&](int x1, int y1, int x2, int y2, long long c) {
d[x1][y1] += c;
d[x2 + 1][y1] -= c;
d[x1][y2 + 1] -= c;
d[x2 + 1][y2 + 1] += c;
};
复杂度:
- 单次修改
O(1),恢复O(nm)。
典型题型:
- 矩阵区间染色、增量、覆盖。
易错点:
- 四个角符号写错。
推荐练习:
- 写一个网格批量加值模板。
双指针
对撞指针
什么时候用:
- 数组有序。
- 左右两端向中间靠拢。
核心思路:
- 用两个端点维护答案。
模板:
cpp
sort(a.begin(), a.end());
int l = 0, r = (int)a.size() - 1;
while (l < r) {
long long sum = 1LL * a[l] + a[r];
if (sum == target) break;
if (sum < target) l++;
else r--;
}
复杂度:
- 排序
O(nlogn),扫描O(n)。
典型题型:
- 两数和、最接近和、配对问题。
易错点:
- 忘了排序。
推荐练习:
- 两数和与最接近目标值两类题都练一题。
快慢指针
什么时候用:
- 需要原地去重、压缩、稳定筛选。
核心思路:
- 快指针负责扫描,慢指针负责保留有效部分。
模板:
cpp
sort(a.begin(), a.end());
int j = 0;
for (int i = 0; i < (int)a.size(); i++) {
if (i == 0 || a[i] != a[i - 1]) {
a[j++] = a[i];
}
}
复杂度:
O(n),若含排序则总体O(nlogn)。
典型题型:
- 去重、原地删除元素。
易错点:
- 误以为快慢指针只能用在链表。
推荐练习:
- 写一个原地去重模板。
滑动窗口
什么时候用:
- 连续区间中找最短 / 最长满足条件的段。
核心思路:
- 右端点负责扩展。
- 条件不满足时左端点收缩。
模板:
cpp
int l = 0;
for (int r = 0; r < n; r++) {
// 加入 a[r]
while (窗口不合法) {
// 移除 a[l]
l++;
}
// 更新答案
}
复杂度:
- 一般
O(n)。
典型题型:
- 最长不重复子串。
- 最短满足和不小于
k的区间。
易错点:
- 更新答案时机错误。
推荐练习:
- 练一道最长无重复子串和一道最短区间题。
双指针题的识别技巧
什么时候用:
- 读题阶段。
核心思路:
- 看到"连续""区间""有序""最短 / 最长"时就要警觉。
| 题目关键词 | 是否考虑双指针 |
|---|---|
| 连续子数组 / 子串 | 强烈考虑 |
| 有序数组 | 强烈考虑 |
| 最短满足条件区间 | 强烈考虑 |
| 最长满足条件区间 | 强烈考虑 |
典型题型:
- 区间统计、字符串扫描、数组配对。
易错点:
- 把本来该滑窗的问题写成了二重循环。
推荐练习:
- 对自己的题目集做一次"哪些题能用双指针"的分类。
位运算
位判断与位修改
什么时候用:
- 处理二进制位。
- 判断奇偶。
核心思路:
1 << k表示第k位。- 大位数时用
1LL << k。
模板:
cpp
bool isOdd = (x & 1);
bool bit = ((x >> k) & 1);
x |= (1LL << k);
x &= ~(1LL << k);
x ^= (1LL << k);
复杂度:
- 单次
O(1)。
典型题型:
- 状态压缩、位判断、子集枚举。
易错点:
1 << 40会出问题,要写1LL << 40。
推荐练习:
- 写一个输出整数二进制某几位状态的小程序。
lowbit
什么时候用:
- 树状数组、二进制最低位处理。
核心思路:
x & -x取出最低位的1。
模板:
cpp
int lowbit(int x) {
return x & -x;
}
复杂度:
O(1)。
典型题型:
- 树状数组。
易错点:
- 不懂为什么是最低位,导致后续树状数组难理解。
推荐练习:
- 输出若干数字的
lowbit结果。
子集枚举
什么时候用:
- 状态压缩、枚举一个集合的所有子集。
核心思路:
- 子集枚举是位运算的经典应用。
模板:
cpp
for (int mask = 0; mask < (1 << n); mask++) {
// mask 表示一个子集
}
for (int s = mask; s; s = (s - 1) & mask) {
// s 是 mask 的非空子集
}
复杂度:
- 全部子集是
O(2^n)。
典型题型:
- 小规模状态压缩、选或不选问题。
易错点:
n稍大就不能乱用。
推荐练习:
- 枚举
n <= 20的子集类题。
前缀异或
异或也能做前缀
什么时候用:
- 区间异或。
- 奇偶性 / 状态切换类题。
核心思路:
- 异或满足可逆性。
模板:
cpp
vector<int> pre(n + 1, 0);
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] ^ a[i];
}
复杂度:
- 预处理
O(n),查询O(1)。
典型题型:
- 区间异或、前缀状态统计。
易错点:
- 把异或和加法的性质混着用。
推荐练习:
- 做一道区间异或题。
离散化
为什么要离散化
什么时候用:
- 值域非常大,但实际不同值不多。
核心思路:
- 把大值映射成小编号,保留相对大小关系。
模板:
cpp
vector<int> alls = a;
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
auto getId = [&](int x) {
return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
};
复杂度:
O(nlogn)。
典型题型:
- 树状数组、线段树、区间统计。
易错点:
- 只存原数组值,漏掉查询值和边界值。
推荐练习:
- 做一道坐标范围巨大但数据个数不多的题。
离散化题的注意事项
什么时候用:
- 准备把离散化接到数据结构时。
核心思路:
- 所有将来会被访问的值都要加入离散集合。
清单:
- 原数组值是否都加入。
- 查询中的值是否加入。
- 区间端点变化后的值是否加入。
- 编号从
0还是1开始。
典型题型:
- 区间计数、前缀统计、离线题。
易错点:
- 只离散原始点,漏掉操作涉及的新点。
推荐练习:
- 做一道带修改和查询的离散化题。
本章常见题型识别
| 题型现象 | 优先工具 |
|---|---|
| 多次区间和查询 | 前缀和 |
| 多次区间加值 | 差分 |
| 连续区间最短 / 最长 | 滑动窗口 |
| 有序数组配对 | 对撞指针 |
| 原地去重 | 快慢指针 |
| 状态压缩 / 位判断 | 位运算 |
| 值域大但值少 | 离散化 |
本章易错点总表
| 错误类型 | 典型错误 | 怎么避免 |
|---|---|---|
| 前缀和越界 | s[l - 1] 时 l = 0 |
统一下标体系 |
| 差分越界 | r + 1 没多开一位 |
数组开 n + 2 |
| 窗口死循环 | 左右指针更新条件错 | 固定模板并手推样例 |
| 位运算溢出 | 1 << 40 |
写 1LL << 40 |
| 离散化漏点 | 只存原值不存查询值 | 先收集所有会访问的值 |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 一维前缀和 | 区间和模板 | 题库首页 搜索"前缀和" |
| 2 | 二维前缀和 | 容斥和矩阵坐标 | 题库首页 搜索"二维前缀和" |
| 3 | 一维差分 | 区间修改 | 题库首页 搜索"差分" |
| 4 | 滑动窗口 | 最长 / 最短区间 | 题库首页 搜索"滑动窗口" |
| 5 | 双指针配对 | 有序数组扫描 | 题库首页 搜索"双指针" |
| 6 | 位运算基础 | 奇偶、位判断、 lowbit | 题库首页 搜索"位运算" |
| 7 | 子集枚举 | 状态压缩入门 | 题库首页 搜索"状态压缩" |
| 8 | 离散化 | 值域压缩 | 题库首页 搜索"离散化" |
| 9 | 高频真题组合 | 前缀和 / 双指针综合 | 蓝桥杯真题卷 |
| 10 | 限时回顾 | 不看资料手写 5 个模板 | 附录 |
区间题速判表
| 题目现象 | 首选工具 |
|---|---|
| 多次区间求和 | 前缀和 |
| 多次区间增加 | 差分 |
| 矩阵子区域求和 | 二维前缀和 |
| 矩阵批量加值 | 二维差分 |
| 连续区间最短 / 最长 | 滑动窗口 |
| 有序数组配对 | 对撞指针 |
双指针与位运算自测
-
- 我能区分对撞指针和滑动窗口吗?
-
- 我知道滑动窗口为什么常是
O(n)吗?
- 我知道滑动窗口为什么常是
-
- 我会写前缀和区间公式吗?
-
- 我会写差分恢复原数组吗?
-
- 我知道什么时候该离散化吗?
-
- 我会写
lowbit吗?
- 我会写
-
- 我知道
1LL << k的必要性吗?
- 我知道
-
- 我知道前缀异或和前缀和的区别吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 这一章是蓝桥杯最值得反复回看的板块之一。
- 真正掌握的标准不是"眼熟",而是你能在没有任何提示的情况下,把前缀和、差分、滑动窗口、离散化模板手敲出来。
- 如果你只能重点看几章,这一章一定排在前列。
四、搜索专题:DFS、BFS、回溯与剪枝
本章适合谁
- 一看到"枚举所有方案"就头皮发麻的人。
- 会写递归,但经常爆栈、漏恢复状态、重复搜索的人。
- 网格题、排列组合题、最短步数题做得不稳定的人。
建议前置知识
- 已掌握前 3 章。
- 会写基础递归和数组遍历。
本章目标
| 板块 | 目标 |
|---|---|
| 递归基础 | 理清参数、出口和转移 |
| DFS / 回溯 | 会写排列、组合、子集、棋盘类搜索 |
| BFS | 会写最短步数和网格扩展 |
| 记忆化搜索 | 会减少重复状态 |
| 剪枝 | 能主动砍掉无效搜索 |
| 状态恢复 | 不再频繁因回溯细节 WA |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 枚举所有方案 | DFS / 回溯 |
| 无权图最少步数 | BFS |
| 棋盘走格子 / 连通块 | DFS / BFS |
| 组合、排列、子集 | 回溯 |
| 同一状态会被反复求值 | 记忆化搜索 |
| 搜索空间很大但可提前判断无效 | 剪枝 |
递归基础
写递归之前先想什么
什么时候用:
- 写任何 DFS、回溯、树递归之前。
核心思路:
- 递归并不神秘,本质是"当前层做一点事,剩下交给下一层"。
- 真正常错的不是递归本身,而是递归的定义不清楚。
你至少要回答 4 个问题:
- 当前函数参数表示什么。
- 什么时候停止。
- 当前层做哪些选择。
- 返回上一层前要不要恢复状态。
模板:
cpp
void dfs(int step) {
if (step == n) {
// 记录答案
return;
}
for (int i = 0; i < k; i++) {
// 做选择
dfs(step + 1);
// 撤销选择
}
}
复杂度:
- 常常取决于分支数和递归深度。
典型题型:
- 排列、组合、图遍历、树遍历。
易错点:
- 出口漏写。
- 参数意义混乱。
- 该恢复的状态没恢复。
推荐练习:
- 不看资料写一个最基础的"从 1 到 n 选数"的回溯模板。
递归与迭代怎么选
什么时候用:
- 不确定该用 DFS 还是循环 / 栈模拟时。
核心思路:
- 如果问题天然是"层层展开"的,就可以优先递归。
- 如果深度很深、状态更新复杂,也可以考虑手写栈或 BFS。
判断建议:
| 情况 | 更推荐 |
|---|---|
| 结构天然分层 | 递归 |
| 要穷举所有方案 | 递归 / 回溯 |
| 要最短步数 | BFS |
| 深度非常深 | 迭代或手动栈 |
典型题型:
- 树搜索、图搜索、棋盘搜索。
易错点:
- 明明该 BFS 却用 DFS 找最短步数。
推荐练习:
- 比较一题连通块统计用 DFS 和 BFS 的两种写法。
DFS 与回溯
排列型回溯
什么时候用:
- 要从
1~n里选出一个排列。 - 每个元素只能用一次。
核心思路:
used[i]表示某个元素是否已经被使用。- 路径数组保存当前决策。
模板:
cpp
int n;
vector<int> path;
bool used[25];
void dfs(int step) {
if (step == n) {
// 记录 path
return;
}
for (int i = 1; i <= n; i++) {
if (used[i]) continue;
used[i] = true;
path.push_back(i);
dfs(step + 1);
path.pop_back();
used[i] = false;
}
}
复杂度:
O(n * n!)量级。
典型题型:
- 全排列、小规模枚举。
易错点:
- 忘记恢复
used[i]。 - 只记录答案不恢复路径。
推荐练习:
- 写出
1~4全排列并检查输出顺序。
组合型回溯
什么时候用:
- 从
1~n中选出k个。
核心思路:
- 用
start控制后续选择范围,避免重复。
模板:
cpp
int n, k;
vector<int> path;
void dfs(int start) {
if ((int)path.size() == k) {
// 记录答案
return;
}
for (int i = start; i <= n; i++) {
path.push_back(i);
dfs(i + 1);
path.pop_back();
}
}
复杂度:
- 约为组合数量级。
典型题型:
- 组合枚举、子集枚举的一部分。
易错点:
start更新错导致重复选择。
推荐练习:
- 枚举从
1~5中选3个数的所有方案。
子集型回溯
什么时候用:
- 每个元素只有"选"或"不选"两种决策。
核心思路:
- 当前层做两种决策:选 / 不选。
模板:
cpp
void dfs(int u) {
if (u > n) {
// 记录答案
return;
}
// 不选
dfs(u + 1);
// 选
path.push_back(u);
dfs(u + 1);
path.pop_back();
}
复杂度:
O(2^n)。
典型题型:
- 子集枚举、选或不选模型。
易错点:
- 两个分支顺序无所谓,但恢复状态不能漏。
推荐练习:
- 枚举一个长度为
n的集合所有子集。
棋盘 / 网格 DFS
什么时候用:
- 连通块统计、岛屿问题、路径搜索。
核心思路:
- 方向数组 + 边界判断 + 访问标记。
模板:
cpp
int n, m;
char g[105][105];
bool vis[105][105];
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void dfs(int x, int y) {
vis[x][y] = true;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (vis[nx][ny] || g[nx][ny] == '#') continue;
dfs(nx, ny);
}
}
复杂度:
- 一般每个点访问一次,为
O(nm)。
典型题型:
- 连通块、岛屿数量、 flood fill。
易错点:
- 访问标记时机错误导致重复搜索。
推荐练习:
- 做两道连通块题,一题 DFS,一题 BFS。
剪枝
为什么要剪枝
什么时候用:
- 搜索空间太大,纯暴力不可能全部搜完。
核心思路:
- 剪枝不是瞎砍,而是提前排除一定无效的分支。
常见剪枝类型:
| 类型 | 说明 |
|---|---|
| 越界剪枝 | 明显不合法的状态直接停 |
| 可行性剪枝 | 当前状态已经不可能满足答案 |
| 最优性剪枝 | 当前代价已不优于已知答案 |
| 对称性剪枝 | 等价状态不重复搜 |
模板:
cpp
if (当前状态明显不可能得到更优答案) return;
复杂度:
- 剪枝不改变理论最坏复杂度,但通常极大提升实际效率。
典型题型:
- 回溯、棋盘搜索、最优解搜索。
易错点:
- 剪早了会错,剪晚了没效果。
推荐练习:
- 把一题暴力回溯加上至少两种剪枝。
剪枝的设计顺序
什么时候用:
- 你已经有一个正确但慢的搜索时。
核心思路:
- 先保正确,再谈剪枝。
- 最好先写无剪枝版本用于对拍。
建议顺序:
- 先写正确搜索。
- 找明显不合法状态。
- 找不可能更优的状态。
- 再考虑高级剪枝。
典型题型:
- N 皇后、分组、装箱、最优解搜索。
易错点:
- 还没保证对就先乱加剪枝。
推荐练习:
- 复盘一题加剪枝前后的耗时变化。
BFS
网格 BFS
什么时候用:
- 无权网格最少步数。
- 每次移动代价相同。
核心思路:
- 一层一层扩展。
- 第一次到达某点时,往往就是最短距离。
模板:
cpp
int n, m;
char g[105][105];
int dist[105][105];
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
queue<pair<int, int>> q;
memset(dist, -1, sizeof dist);
dist[sx][sy] = 0;
q.push({sx, sy});
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (g[nx][ny] == '#') continue;
if (dist[nx][ny] != -1) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
复杂度:
O(nm)。
典型题型:
- 迷宫、最短步数、棋盘移动。
易错点:
- 入队前不标记,导致重复入队。
推荐练习:
- 至少练 3 道网格最短步数题。
图上的 BFS
什么时候用:
- 无权图最短路、层次遍历。
核心思路:
- 邻接表存图,距离数组记录层数。
模板:
cpp
vector<vector<int>> g(n + 1);
vector<int> dist(n + 1, -1);
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (dist[v] != -1) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
复杂度:
O(n + m)。
典型题型:
- 社交关系层数、最少转换次数。
易错点:
- 图没建好, BFS 再正确也没用。
推荐练习:
- 做一道普通图最短步数题。
多源 BFS
什么时候用:
- 多个起点同时扩散。
核心思路:
- 把所有起点一起入队。
模板:
cpp
for (auto [x, y] : sources) {
dist[x][y] = 0;
q.push({x, y});
}
复杂度:
- 和普通 BFS 同阶。
典型题型:
- 多个感染源、多个火源、最近距离问题。
易错点:
- 多个源点初始化不完整。
推荐练习:
- 做一道"最近的某类格子"题。
记忆化搜索
为什么要记忆化
什么时候用:
- 暴力 DFS 会重复计算很多状态。
核心思路:
- "搜过就记住"。
- 本质是 DFS 写法的动态规划。
模板:
cpp
vector<int> dp(n, -1);
int dfs(int x) {
if (dp[x] != -1) return dp[x];
int res = 1;
for (int y = x + 1; y < n; y++) {
if (a[y] > a[x]) {
res = max(res, dfs(y) + 1);
}
}
return dp[x] = res;
}
复杂度:
- 取决于状态数和转移数量。
典型题型:
- DAG、序列转移、区间转移。
易错点:
- 缓存数组初始化不清晰。
- 状态定义不完整。
推荐练习:
- 把一题暴力 DFS 改成记忆化搜索。
Flood Fill
连通块统计
什么时候用:
- 统计网格中若干连通区域。
核心思路:
- 对每个未访问合法点发起一次 DFS / BFS。
- 每发起一次,就发现一个新连通块。
模板:
cpp
int cnt = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!vis[i][j] && g[i][j] == '1') {
cnt++;
dfs(i, j);
}
}
}
复杂度:
O(nm)。
典型题型:
- 岛屿数量、区域统计、同色块统计。
易错点:
- 忘记把发起搜索计数和具体扩展逻辑分开。
推荐练习:
- 练一道岛屿数量题。
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 要输出所有可行方案 | 回溯 |
| 最少步数、每步代价相同 | BFS |
| 连通块 | DFS / BFS |
| 小范围排列组合 | 回溯 |
| 同状态反复出现 | 记忆化搜索 |
| 搜索空间爆炸 | 剪枝 |
本章易错点总表
| 错误类型 | 具体表现 | 避免方式 |
|---|---|---|
| 漏恢复状态 | used、路径数组没还原 |
写出"做选择 / 递归 / 撤销选择"固定结构 |
| 重复搜索 | vis 标记时机错误 |
入队或进入递归时立即标记 |
| BFS 写成 DFS | 求最少步数时还在深搜 | 看到"最短步数"优先 BFS |
| 剪枝错杀 | 提前返回导致漏答案 | 先写无剪枝版本对拍 |
| 栈溢出 | 深度太深仍用递归 | 考虑迭代或 BFS |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 递归基础 | 理清参数与出口 | 题库首页 搜索"递归" |
| 2 | 排列回溯 | used 与状态恢复 |
题库首页 搜索"排列" |
| 3 | 组合回溯 | start 控制范围 |
题库首页 搜索"组合" |
| 4 | 连通块 DFS | 网格 + 访问标记 | 题库首页 搜索"连通块" |
| 5 | 网格 BFS | 最少步数 | 题库首页 搜索"BFS" |
| 6 | 图 BFS | 邻接表遍历 | 题库首页 搜索"最短步数" |
| 7 | 记忆化搜索 | 避免重复状态 | 题库首页 搜索"记忆化搜索" |
| 8 | 剪枝回溯 | 搜索优化 | 题库首页 搜索"剪枝" |
| 9 | 真题搜索题 | 综合搜索能力 | 蓝桥杯真题卷 |
| 10 | 手敲模板 | DFS / BFS / 回溯连写 | 附录 |
搜索题速判表
| 题目现象 | 首选方法 |
|---|---|
| 输出所有方案 | 回溯 |
| 最少步数 | BFS |
| 连通块数量 | DFS / BFS |
| 小规模排列组合 | 回溯 |
| 同状态反复出现 | 记忆化搜索 |
| 搜索空间爆炸但可提前排除 | 剪枝 |
搜索自测问题
-
- 我能说清 DFS 参数表示什么吗?
-
- 我会写递归出口吗?
-
- 我知道什么时候该 BFS 吗?
-
- 我会在回溯里恢复状态吗?
-
- 我会在 BFS 里处理访问标记吗?
-
- 我知道记忆化搜索适合什么问题吗?
-
- 我知道什么时候剪枝是安全的吗?
-
- 我能区分"静态遍历"和"最短步数搜索"吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 搜索专题不是只靠背模板,它更考验你是否能把状态定义清楚。
- 赛场上最容易出错的不是搜索思路,而是访问标记、恢复状态和边界判断。
- 如果你能稳稳写出排列回溯、连通块 DFS、网格 BFS,这一章就已经过半了。
五、动态规划专题
本章适合谁
- 看到 DP 就想套公式,但经常套错的人。
- 会写搜索,却不会把搜索压成状态转移的人。
- 对"状态、转移、初始化、答案"这四件事还不够稳定的人。
建议前置知识
- 已掌握前 4 章。
- 能理解数组与递归。
本章目标
| 板块 | 目标 |
|---|---|
| DP 思维 | 能从题目中找到状态定义 |
| 线性 DP | 会写基础一维转移 |
| 背包 DP | 会写 0-1、完全和多重背包主线 |
| 网格 DP | 会处理路径和矩阵类转移 |
| 序列 DP | 会写 LIS、LCS 等高频模型 |
| 区间 DP | 知道拆区间的基本方式 |
| 进阶 DP | 了解状态压缩 / 数位 DP 的应用边界 |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 要求最优值且包含"前 i 个"结构 | 线性 DP |
| 选或不选物品 | 背包 |
| 棋盘从起点到终点走法 / 最值 | 网格 DP |
| 序列匹配、最长上升、最长公共 | 序列 DP |
| 区间合并、括号、石子合并 | 区间 DP |
| 状态很少、可用位表示 | 状态压缩 DP |
| 与数位、上界限制、计数有关 | 数位 DP |
DP 的通用做题流程
先问自己这 4 个问题
什么时候用:
- 写任何 DP 之前。
核心思路:
- DP 的关键不是模板,而是状态设计。
必答问题:
dp[i]或dp[i][j]表示什么。- 它如何从更小状态转移而来。
- 初始值是什么。
- 最终答案在哪个状态里。
模板:
cpp
// 例:dp[i] 表示前 i 个位置的最优值
for (int i = 1; i <= n; i++) {
dp[i] = ...;
for (int j = 0; j < i; j++) {
dp[i] = max(dp[i], dp[j] + ...);
}
}
复杂度:
- 由状态数和转移数决定。
典型题型:
- 所有 DP 题。
易错点:
- 状态定义模糊,导致转移也模糊。
推荐练习:
- 每做一道 DP 题,先只写状态定义和转移,不急着写代码。
DP 与搜索的关系
什么时候用:
- 你能写 DFS,但不会写 DP 时。
核心思路:
- 很多 DP 可以看成"记忆化搜索的递推化"。
- 先会搜,再会记,再会推。
对照关系:
| 搜索视角 | DP 视角 |
|---|---|
| 当前状态是什么 | dp 状态定义 |
| 从这里能走到哪里 | 转移方程 |
| 搜过就记住 | 记忆化搜索 |
| 按顺序直接推 | 递推 DP |
典型题型:
- 序列转移、 DAG、区间问题。
易错点:
- 明明有重叠子问题,还只会暴力搜索。
推荐练习:
- 把一题记忆化搜索改写成递推 DP。
线性 DP
基础一维 DP
什么时候用:
- 状态按顺序推进。
- 每个位置只和前面若干位置相关。
核心思路:
dp[i]表示处理到第i个位置时的最优值 / 方案数。
模板:
cpp
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
dp[i] = dp[i - 1];
// 其他转移
}
复杂度:
- 一般是
O(n)或O(n^2)。
典型题型:
- 爬楼梯、路径计数、最优和问题。
易错点:
- 初始状态没想清楚。
推荐练习:
- 写一个"走台阶 / 最小代价"类题。
最长上升子序列 LIS
什么时候用:
- 看到"最长上升 / 下降子序列"。
核心思路:
- 经典
O(n^2)写法:dp[i]表示以i结尾的 LIS 长度。
模板:
cpp
vector<int> dp(n, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int ans = *max_element(dp.begin(), dp.end());
复杂度:
O(n^2)。
典型题型:
- LIS、最长单调子序列变形。
易错点:
- 子序列和子数组概念混淆。
推荐练习:
- 至少练 2 道 LIS 及其变形题。
最长公共子序列 LCS
什么时候用:
- 比较两个字符串 / 数组的共同结构。
核心思路:
dp[i][j]表示前i个和前j个的 LCS 长度。
模板:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if (a[i] == b[j]) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
复杂度:
O(nm)。
典型题型:
- 字符串相似度、序列匹配。
易错点:
- 下标从
1开始更稳,别乱混。
推荐练习:
- 写一道 LCS 模板题。
背包 DP
0-1 背包
什么时候用:
- 每件物品最多选一次。
核心思路:
dp[j]表示体积不超过j时的最大价值。- 逆序枚举容量,保证每件物品只用一次。
模板:
cpp
vector<long long> dp(V + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
复杂度:
O(nV)。
典型题型:
- 选物品、资源分配、限制容量最大价值。
易错点:
- 顺序写反就会变成完全背包。
推荐练习:
- 先练二维,再练一维滚动压缩。
完全背包
什么时候用:
- 每件物品可以选无限次。
核心思路:
- 顺序枚举容量,让同一物品可以重复使用。
模板:
cpp
vector<long long> dp(V + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
复杂度:
O(nV)。
典型题型:
- 硬币问题、无限次购买。
易错点:
- 和 0-1 背包的循环方向混。
推荐练习:
- 对比写一遍 0-1 和完全背包。
多重背包
什么时候用:
- 每件物品有固定件数限制。
核心思路:
- 最基础写法是拆成有限件 0-1 背包。
- 考场上如果数据不大,朴素拆分也够用。
模板:
cpp
for (int i = 1; i <= n; i++) {
for (int k = 1; k <= cnt[i]; k++) {
for (int j = V; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
}
复杂度:
- 与总件数相关。
典型题型:
- 限定数量物品选择。
易错点:
- 数据大时朴素拆分可能超时。
推荐练习:
- 先做数据不大的多重背包题。
分组背包
什么时候用:
- 每组物品最多选一个。
核心思路:
- 对每一组,枚举容量,再枚举组内选哪个。
模板:
cpp
for (int i = 1; i <= g; i++) {
for (int j = V; j >= 0; j--) {
for (auto [vol, val] : group[i]) {
if (j >= vol) {
dp[j] = max(dp[j], dp[j - vol] + val);
}
}
}
}
复杂度:
- 取决于总物品数和容量。
典型题型:
- 每组选一项、课程安排、套餐选择。
易错点:
- 组内物品之间不能互相影响,循环顺序要稳。
推荐练习:
- 做一道分组背包题。
网格 DP
路径计数与最值
什么时候用:
- 网格只能向右 / 向下走。
核心思路:
dp[i][j]表示到(i,j)的方案数或最优值。
模板:
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
}
}
复杂度:
O(nm)。
典型题型:
- 最大奖励路径、最小路径和、走法数。
易错点:
- 起点初始化没处理。
- 不可达格子没单独判断。
推荐练习:
- 练一题最小路径和和一题路径计数。
区间 DP
区间 DP 的核心框架
什么时候用:
- 问题和一个区间的合并顺序有关。
核心思路:
- 先枚举区间长度,再枚举左端点,再枚举分界点。
模板:
cpp
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
dp[l][r] = INF;
for (int k = l; k < r; k++) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + cost(l, r));
}
}
}
复杂度:
- 常见
O(n^3)。
典型题型:
- 石子合并、括号匹配、区间合并。
易错点:
- 枚举顺序不对,转移依赖还没算出来。
推荐练习:
- 做一道经典石子合并题。
进阶 / 选学
状态压缩 DP
什么时候用:
- 状态数量不大,能用位表示。
核心思路:
- 用
mask表示集合状态。
模板:
cpp
for (int mask = 0; mask < (1 << n); mask++) {
// 转移
}
复杂度:
- 常见是
O(n2^n)。
典型题型:
- 小规模旅行、选点覆盖。
易错点:
n稍大就会爆。
推荐练习:
- 了解即可,作为 A 组进阶。
数位 DP
什么时候用:
- 题目和区间数字性质、上界限制有关。
核心思路:
- 按位枚举,带上"前缀是否贴上界"等状态。
典型题型:
- 统计区间内满足某种数字性质的数。
易错点:
- 初学者容易在状态设计上迷失。
推荐练习:
- 先看思路,不要求一开始就手写很快。
DP 常见失误
| 错误类型 | 典型表现 | 解决方式 |
|---|---|---|
| 状态定义模糊 | dp[i] 含义自己都说不清 |
先写中文定义 |
| 初始化错误 | dp 默认值不合理 |
明确起点和不可达状态 |
| 转移顺序错 | 背包循环方向写反 | 固定记忆 0-1 逆序、完全正序 |
| 答案位置错 | 不知道最终该输出什么 | 一开始就写清楚"答案在哪" |
| 维度不必要地过高 | 代码冗长又慢 | 优先考虑是否能滚动优化 |
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 前 i 个元素的最优值 | 线性 DP |
| 选物品受容量限制 | 背包 DP |
| 网格最优路径 | 网格 DP |
| 字符串 / 序列比较 | 序列 DP |
| 合并区间顺序影响结果 | 区间 DP |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 一维线性 DP | 状态定义、初始化 | 题库首页 搜索"动态规划" |
| 2 | LIS / LCS | 序列 DP | 题库首页 搜索"最长上升子序列" |
| 3 | 0-1 背包 | 循环方向 | 题库首页 搜索"0-1 背包" |
| 4 | 完全背包 | 正序转移 | 题库首页 搜索"完全背包" |
| 5 | 分组 / 多重背包 | 背包变形 | 题库首页 搜索"分组背包" |
| 6 | 网格 DP | 二维状态 | 题库首页 搜索"网格 DP" |
| 7 | 区间 DP | 枚举顺序 | 题库首页 搜索"区间 DP" |
| 8 | 记忆化搜索转 DP | 搜索和 DP 互转 | 题库首页 搜索"记忆化搜索" |
| 9 | 真题 DP 组合 | 综合练习 | 蓝桥杯真题卷 |
| 10 | 模板压缩 | 不看资料手写背包与 LIS | 附录 |
DP 模型对照速表
| 题目现象 | 最可能模型 | 关键状态 |
|---|---|---|
| 前 i 个元素最优值 | 线性 DP | dp[i] |
| 每件物品选或不选 | 0-1 背包 | dp[j] |
| 每件物品无限次选 | 完全背包 | dp[j] |
| 每组最多选一个 | 分组背包 | dp[j] |
| 最长上升子序列 | 序列 DP | dp[i] |
| 最长公共子序列 | 二维序列 DP | dp[i][j] |
| 从左上到右下最优 | 网格 DP | dp[i][j] |
| 区间合并代价 | 区间 DP | dp[l][r] |
| 状态很少可二进制表示 | 状态压缩 DP | dp[mask] |
| 区间数字性质统计 | 数位 DP | dp[pos][state][limit] |
背包循环方向口令
- 0-1 背包:容量逆序。
- 完全背包:容量正序。
- 多重背包:先想能不能拆成 0-1。
- 分组背包:容量逆序,组内枚举选择。
- 看不清循环方向时,先写二维版本再压缩。
DP 常见反例与提醒
- 把子序列当成子数组。
- 把"最多一次"写成了"无限次"。
- 把初值设成
0,但实际上应该是负无穷或大正数。 - 网格不可达状态没有单独处理。
- 区间 DP 枚举顺序写反,导致转移使用了未计算状态。
- 最后输出
dp[n],但题目实际答案在max(dp[i])。 - 方案数题忘记取模。
- 状态定义过大,内存直接爆掉。
- 本来能滚动优化,却写成大数组浪费空间。
- 状态不完整,导致转移缺条件。
DP 自测清单
-
- 我能用一句中文说清
dp的定义吗?
- 我能用一句中文说清
-
- 初始状态是哪个?
-
- 不可达状态该设成什么?
-
- 转移是从哪些更小状态来的?
-
- 答案一定在
dp[n]吗?
- 答案一定在
-
- 能否滚动数组优化?
-
- 循环方向有没有受"是否可重复使用"影响?
-
- 这是子序列还是子数组?
-
- 是否要取模?
-
- 二维 DP 是否能降一维?
-
- 题目是求最大值、最小值还是方案数?
-
- 状态中是否漏了某个关键信息?
-
- 本题更适合递推还是记忆化搜索?
-
- 如果写暴力搜索,状态会重复吗?
-
- 区间 DP 的长度枚举顺序是否正确?
-
- 网格 DP 有没有障碍物?
-
- 是否有必要预处理前缀信息来辅助 DP?
-
- 我的
dp数组大小够吗?
- 我的
-
- 当前写法复杂度能过吗?
-
- 这个模型我以前见过哪个经典原型?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- DP 真正难的地方不是代码,而是状态定义。
- 如果你总想直接背板子,往往会在变形题上卡住。
- 先把线性 DP、背包、网格 DP、 LIS / LCS 稳住,再去冲区间 DP 和进阶内容。
六、图论与常用数据结构
本章适合谁
- 想把蓝桥杯中高频专题补齐的人。
- 对并查集、最短路、拓扑排序、单调结构只会一点点的人。
- 想兼顾 A 组常见进阶内容,但不想一上来就学太重型算法的人。
建议前置知识
- 已掌握前 5 章。
- 会 BFS、排序和基础 DP 更好。
本章目标
| 板块 | 目标 |
|---|---|
| 图存储与遍历 | 会建图,知道邻接表最常用 |
| 并查集 | 会判断连通块和合并集合 |
| 拓扑排序 | 会处理 DAG 先后关系 |
| 最短路 | 会 BFS 最短路和 Dijkstra |
| 最小生成树 | 会 Kruskal |
| 单调栈 / 队列 | 会做最近更大 / 更小值和滑窗最值 |
| 树状数组 / 线段树 | 了解并能写高频模板 |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 点和边组成关系网络 | 图 |
| 判断是否连通 / 合并集合 | 并查集 |
| 存在先后依赖 | 拓扑排序 |
| 带权最短路且边权非负 | Dijkstra |
| 连接所有点代价最小 | 最小生成树 |
| 求每个位置左边 / 右边最近更大更小 | 单调栈 |
| 区间最值滑动 | 单调队列 |
| 单点修改、前缀统计 | 树状数组 |
| 区间修改 / 区间查询 | 线段树 |
图的存储
邻接表
什么时候用:
- 大多数图题。
核心思路:
- 只存实际存在的边,空间更省。
模板:
cpp
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u); // 无向图
}
复杂度:
- 遍历复杂度与边数成正比。
典型题型:
- 图搜索、最短路、拓扑排序。
易错点:
- 有向图和无向图建边方式不同。
推荐练习:
- 手写一个无向图和有向图的邻接表模板。
边结构体建图
什么时候用:
- 边带权。
核心思路:
- 用
pair或结构体存目标点和边权。
模板:
cpp
vector<vector<pair<int, int>>> g(n + 1);
g[u].push_back({v, w});
g[v].push_back({u, w});
复杂度:
- 遍历
O(n + m)量级。
典型题型:
- Dijkstra、最短路、最小生成树的预处理。
易错点:
- 把点和权写反。
推荐练习:
- 写一个带权图 BFS / Dijkstra 起手模板。
并查集
并查集的作用
什么时候用:
- 需要动态维护若干集合是否连通。
核心思路:
find(x)找根。merge(a, b)合并两个集合。
模板:
cpp
vector<int> fa(n + 1);
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void merge(int a, int b) {
a = find(a);
b = find(b);
if (a != b) fa[a] = b;
}
初始化:
cpp
for (int i = 1; i <= n; i++) fa[i] = i;
复杂度:
- 均摊接近常数。
典型题型:
- 连通块判断。
- 动态合并关系。
- Kruskal 最小生成树。
易错点:
- 忘记路径压缩。
- 每组数据没重新初始化。
推荐练习:
- 做 2 道基础并查集题和 1 道关系判定题。
拓扑排序
DAG 的先后关系
什么时候用:
- 有向图里存在依赖顺序。
核心思路:
- 入度为
0的点先入队。 - 每删掉一个点,就减少其后继的入度。
模板:
cpp
vector<vector<int>> g(n + 1);
vector<int> deg(n + 1, 0), order;
queue<int> q;
for (int i = 1; i <= n; i++) {
if (deg[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : g[u]) {
if (--deg[v] == 0) q.push(v);
}
}
复杂度:
O(n + m)。
典型题型:
- 课程安排、任务先后、 DAG 判环。
易错点:
order.size() < n说明存在环。
推荐练习:
- 做一道课程安排或拓扑排序模板题。
最短路
无权图最短路
什么时候用:
- 边权都相同,通常为
1。
核心思路:
- 用 BFS。
复杂度:
O(n + m)。
典型题型:
- 最少步数、最少边数。
易错点:
- 一看到图就下意识写 Dijkstra。
推荐练习:
- 对比一题无权图 BFS 和 Dijkstra 的思路差异。
Dijkstra
什么时候用:
- 非负边权最短路。
核心思路:
- 每次取当前距离最小的未确定点,松弛邻边。
- 堆优化是蓝桥杯最常用写法。
模板:
cpp
using PII = pair<long long, int>;
vector<vector<pair<int, int>>> g(n + 1);
vector<long long> dist(n + 1, (long long)4e18);
vector<bool> vis(n + 1, false);
priority_queue<PII, vector<PII>, greater<PII>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto [v, w] : g[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
复杂度:
- 堆优化常见为
O((n + m)logn)。
典型题型:
- 非负带权最短路、路径代价最小。
易错点:
- 有负边时不能直接用 Dijkstra。
- 忘记写
if (vis[u]) continue;。
推荐练习:
- 至少练 2 道标准 Dijkstra 题。
最小生成树
Kruskal
什么时候用:
- 需要连接所有点,且总边权最小。
核心思路:
- 按边权从小到大排序。
- 若边的两个端点不在同一集合,就选它。
模板:
cpp
struct Edge {
int u, v, w;
bool operator < (const Edge& other) const {
return w < other.w;
}
};
sort(edges.begin(), edges.end());
long long ans = 0;
int cnt = 0;
for (auto &e : edges) {
int fu = find(e.u), fv = find(e.v);
if (fu == fv) continue;
fa[fu] = fv;
ans += e.w;
cnt++;
}
复杂度:
- 主要是排序
O(mlogm)。
典型题型:
- 最小生成树、最小连接代价。
易错点:
- 最后要检查是否选到了
n - 1条边。
推荐练习:
- 做一道标准最小生成树题。
单调栈
最近更大 / 更小值
什么时候用:
- 对每个位置找左边 / 右边最近更大或更小元素。
核心思路:
- 维护一个单调的栈。
- 不满足单调性时不断弹栈。
模板:
cpp
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
if (!st.empty()) leftLess[i] = st.top();
st.push(i);
}
复杂度:
O(n)。
典型题型:
- 最近更小值、柱状图、单调结构题。
易错点:
- 栈里存值还是存下标没分清。
推荐练习:
- 练一道最近更小值题和一题柱状图变形。
单调队列
滑动窗口最值
什么时候用:
- 固定长度窗口内求最大 / 最小值。
核心思路:
- 队列里维护候选元素下标。
- 队头永远是当前窗口最优下标。
模板:
cpp
deque<int> q;
for (int i = 0; i < n; i++) {
while (!q.empty() && q.front() <= i - k) q.pop_front();
while (!q.empty() && a[q.back()] <= a[i]) q.pop_back();
q.push_back(i);
if (i >= k - 1) ans.push_back(a[q.front()]);
}
复杂度:
O(n)。
典型题型:
- 滑动窗口最大值 / 最小值。
易错点:
- 队头过期判断和队尾维护顺序写乱。
推荐练习:
- 写一个窗口最大值模板。
树状数组
树状数组能做什么
什么时候用:
- 单点修改,前缀和查询。
- 有时也能做区间修改 / 查询变形。
核心思路:
- 利用
lowbit维护若干前缀块。
模板:
cpp
vector<long long> tr(n + 1, 0);
int lowbit(int x) {
return x & -x;
}
void add(int x, long long v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
long long sum(int x) {
long long res = 0;
for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
return res;
}
复杂度:
- 单次修改 / 查询
O(logn)。
典型题型:
- 动态前缀和、逆序对、离散化后计数。
易错点:
- 下标必须从
1开始更稳。
推荐练习:
- 写一道动态前缀和或逆序对题。
线段树
线段树的定位
什么时候用:
- 区间查询与区间更新需要同时兼顾。
核心思路:
- 把区间递归分成左右两半。
- 信息保存在节点上。
基础模板:
cpp
struct Node {
int l, r;
long long sum;
} tr[N << 2];
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
建树:
cpp
void build(int u, int l, int r) {
tr[u] = {l, r, 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
复杂度:
- 单次操作常见
O(logn)。
典型题型:
- 区间和、区间最值、区间修改。
易错点:
- 初学者容易在递归边界和懒标记上混乱。
推荐练习:
- 先写单点修改 + 区间查询,再学懒标记。
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 连通关系变化 | 并查集 |
| 先后依赖 | 拓扑排序 |
| 非负带权最短路 | Dijkstra |
| 连接所有点总代价最小 | Kruskal |
| 最近更大 / 更小 | 单调栈 |
| 固定窗口最值 | 单调队列 |
| 动态前缀统计 | 树状数组 |
| 区间问题更复杂 | 线段树 |
本章易错点总表
| 错误类型 | 具体表现 | 避免方式 |
|---|---|---|
| 图建错 | 有向图当无向图建 | 先确认边方向 |
| 并查集没初始化 | fa[i] 不是自己 |
每组数据初始化 |
| Dijkstra 乱用 | 存在负边仍照写 | 先看边权范围 |
| Kruskal 少判断 | 没检查是否连通 | 统计选边数是否 n - 1 |
| 单调结构错误 | 队头过期和队尾维护顺序乱 | 套固定模板 |
| 树状数组下标错 | 从 0 开始写 |
统一 1 下标 |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 建图与遍历 | 邻接表熟练度 | 题库首页 搜索"图" |
| 2 | 并查集 | 合并、查根、连通判断 | 题库首页 搜索"并查集" |
| 3 | 拓扑排序 | 入度与 DAG | 题库首页 搜索"拓扑排序" |
| 4 | 无权图最短路 | BFS | 题库首页 搜索"最短路" |
| 5 | Dijkstra | 堆优化最短路 | 题库首页 搜索"Dijkstra" |
| 6 | Kruskal | 排序 + 并查集 | 题库首页 搜索"最小生成树" |
| 7 | 单调栈 | 最近更大 / 更小 | 题库首页 搜索"单调栈" |
| 8 | 单调队列 | 滑窗最值 | 题库首页 搜索"单调队列" |
| 9 | 树状数组 / 线段树 | 动态区间问题 | 题库首页 搜索"树状数组" 或 "线段树" |
| 10 | 真题综合 | 图论 + 数据结构混合 | 蓝桥杯真题卷 |
数据结构选型速表
| 需求 | 优先选择 | 说明 |
|---|---|---|
| 判连通 / 合并集合 | 并查集 | 最稳最常见 |
| 无权最短步数 | BFS | 比 Dijkstra 更轻 |
| 非负带权最短路 | Dijkstra | 蓝桥杯高频 |
| 先后依赖关系 | 拓扑排序 | DAG 题首选 |
| 当前窗口最大值 | 单调队列 | O(n) |
| 最近更大 / 更小 | 单调栈 | O(n) |
| 单点修改、前缀查询 | 树状数组 | 简洁高效 |
| 区间查询、区间修改 | 线段树 | 更通用 |
| 连接所有点最小代价 | Kruskal | 排序 + 并查集 |
图论高频易错场景
- 无向图只加了一条边。
- 有向图误加反向边。
- 点编号从
1开始却按0下标访问。 dist没初始化成足够大的值。- Dijkstra 用在含负边图上。
- 堆里取出的旧状态没过滤。
- Kruskal 做完没判断是否真的连通。
- 并查集每组数据没重新初始化。
- 单调栈里存的是值还是下标混乱。
- 单调队列里窗口过期判断写错。
- 树状数组从
0开始更新导致死循环。 - 线段树区间左右边界不统一。
图论与数据结构自测问题
-
- 我能区分邻接表和邻接矩阵的适用场景吗?
-
- 我会写并查集的
find吗?
- 我会写并查集的
-
- 我知道什么时候更适合 BFS 而不是 Dijkstra 吗?
-
- 我知道拓扑排序能顺便判环吗?
-
- 我会判断最小生成树是否存在吗?
-
- 我能说出单调栈为什么是线性复杂度吗?
-
- 我会写滑动窗口最大值的单调队列吗?
-
- 我知道树状数组的核心操作为什么和
lowbit有关吗?
- 我知道树状数组的核心操作为什么和
-
- 我知道线段树最基础的
pushup是什么吗?
- 我知道线段树最基础的
-
- 我知道比赛里什么时候值得上线段树吗?
-
- 图是有向图还是无向图,我会在写代码前确认吗?
-
- 我会给边带权建图吗?
-
- 我知道 Dijkstra 为何要用小根堆吗?
-
- 我知道最短路题是否真的需要恢复路径吗?
-
- 我有把图题和数据结构题做分类整理吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 这一章内容多,但不需要一次吃完。
- 如果你是 B 组主线复习,优先顺序可以是:并查集 -> Dijkstra -> 拓扑排序 -> 单调栈 -> 树状数组。
- 线段树不要求第一次就完全掌握,但至少要知道它解决什么问题。
七、字符串专题
本章适合谁
- 字符串基础题还能写,但一到 KMP、Trie、哈希就容易乱的人。
- 想把蓝桥杯常见字符串专题补成体系的人。
- 对"字符串题到底该暴力、模拟还是上模板"判断不稳定的人。
建议前置知识
- 已掌握第 1 章的
string和 STL 基础。 - 会前缀和、数组和基本循环。
本章目标
| 板块 | 目标 |
|---|---|
| 字符串基础处理 | 会做切片、统计、匹配、遍历 |
| KMP | 会写前缀函数和匹配过程 |
| Trie | 会做单词插入与查询 |
| 字符串哈希 | 会做子串比较和快速判断 |
| 回文处理 | 会做中心扩展,知道 Manacher 的用途 |
| 题型判断 | 知道什么时候该上模板,什么时候直接模拟 |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 找模式串出现位置 | KMP |
| 维护很多单词 / 前缀 | Trie |
| 快速判断两个子串是否相等 | 字符串哈希 |
| 最长回文子串 | 中心扩展 / Manacher |
| 简单字符统计与转换 | string 模拟 |
字符串基础处理
string 的常见操作
什么时候用:
- 所有字符串题的基本功。
核心思路:
- 先熟悉
size、访问、拼接、截取、比较。
模板:
cpp
string s = "lanqiao";
int n = s.size();
char c = s[0];
string t = s.substr(1, 3);
bool ok = (s == "lanqiao");
复杂度:
- 单字符访问通常
O(1)。 substr一般是线性拷贝。
典型题型:
- 模拟、统计、格式处理。
易错点:
substr(pos, len)的第二个参数是长度,不是右端点。
推荐练习:
- 写一个字符串切片与拼接小程序。
字符串模拟
什么时候用:
- 题目规则不复杂,但涉及字符修改、统计、判定。
核心思路:
- 很多字符串题根本不需要上 KMP。
- 先判断是不是简单模拟。
模板:
cpp
for (char c : s) {
if (isdigit(c)) cntNum++;
if (islower(c)) cntLower++;
}
复杂度:
- 一般
O(n)。
典型题型:
- 统计字符种类、大小写转换、括号匹配预处理。
易错点:
- 该用
getline的题却只写了cin >> s。
推荐练习:
- 做 2 道基础字符串模拟题。
KMP
KMP 的核心问题
什么时候用:
- 想在长文本中找模式串。
- 朴素匹配可能到
O(nm)。
核心思路:
- 失配时不必让模式串完全回到开头。
- 利用已经匹配过的信息,通过
ne数组跳转。
前缀函数 / ne 数组
什么时候用:
- KMP 预处理阶段。
核心思路:
ne[i]记录模式串前缀和后缀的最长公共长度信息。
模板:
cpp
string p;
int m = p.size();
vector<int> ne(m, 0);
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && p[i] != p[j]) j = ne[j - 1];
if (p[i] == p[j]) j++;
ne[i] = j;
}
复杂度:
O(m)。
典型题型:
- 模式串预处理。
易错点:
j = ne[j - 1]这句很容易写错。
推荐练习:
- 先只练
ne数组,不要急着连主串匹配一起写。
主串匹配过程
什么时候用:
- 需要找模式串出现的位置或次数。
核心思路:
- 主串指针只往前走。
- 模式串指针失配时按
ne回退。
模板:
cpp
string s, p;
int n = s.size(), m = p.size();
vector<int> ne(m, 0);
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && p[i] != p[j]) j = ne[j - 1];
if (p[i] == p[j]) j++;
ne[i] = j;
}
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && s[i] != p[j]) j = ne[j - 1];
if (s[i] == p[j]) j++;
if (j == m) {
// 匹配成功,结尾位置是 i
j = ne[j - 1];
}
}
复杂度:
- 总体
O(n + m)。
典型题型:
- 找所有出现位置。
- 统计匹配次数。
易错点:
- 匹配成功后不回退
j,会漏掉重叠匹配。
推荐练习:
- 写一题"找所有匹配位置"的标准 KMP 题。
KMP 的识别技巧
什么时候用:
- 读题阶段。
核心思路:
- 看到"匹配"不代表一定用 KMP,但如果长度很大且需要多次匹配,就要高度考虑。
| 信号 | 是否考虑 KMP |
|---|---|
| 主串很长、模式串也长 | 强烈考虑 |
| 需要找所有匹配位置 | 强烈考虑 |
| 只是一两个短串比较 | 往往不必 |
| 只是判回文 | 更可能不是 KMP |
推荐练习:
- 把做过的字符串题分类成"模拟 / KMP / 哈希 / Trie"。
Trie
Trie 的作用
什么时候用:
- 维护很多字符串。
- 需要快速判断单词是否存在,或统计前缀。
核心思路:
- Trie 是按字符分支的树。
- 每个节点表示一个前缀。
模板:
cpp
const int N = 100000 + 5;
int trie[N][26], cnt[N], idx;
void insert(const string& s) {
int p = 0;
for (char ch : s) {
int u = ch - 'a';
if (!trie[p][u]) trie[p][u] = ++idx;
p = trie[p][u];
}
cnt[p]++;
}
int query(const string& s) {
int p = 0;
for (char ch : s) {
int u = ch - 'a';
if (!trie[p][u]) return 0;
p = trie[p][u];
}
return cnt[p];
}
复杂度:
- 插入 / 查询和字符串长度成正比。
典型题型:
- 单词查找、词频统计、前缀统计。
易错点:
- 总节点数估小了导致越界。
- 字符集不是小写字母时还写死
26。
推荐练习:
- 做 1 道单词插入查询题和 1 道前缀统计题。
Trie 的扩展认识
什么时候用:
- 你已经掌握基础 Trie 后。
核心思路:
- 竞赛里很多更复杂的自动机、异或 Trie 其实也是 Trie 的变形。
- 但蓝桥杯主线先把基础 Trie 吃透更重要。
典型题型:
- 字符串集合管理。
- 二进制前缀树(进阶)。
易错点:
- 还没搞懂普通 Trie 就急着学更复杂的变形。
推荐练习:
- 优先保证普通 Trie 稳定可写。
字符串哈希
哈希的用途
什么时候用:
- 快速比较两个子串是否相等。
- 需要多次判断字符串片段。
核心思路:
- 用一个大进制把字符串映射成数。
- 配合前缀哈希可在
O(1)得到子串哈希值。
模板:
cpp
using ull = unsigned long long;
const ull P = 131;
vector<ull> h(n + 1, 0), p(n + 1, 1);
for (int i = 1; i <= n; i++) {
h[i] = h[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
auto getHash = [&](int l, int r) {
return h[r] - h[l - 1] * p[r - l + 1];
};
复杂度:
- 预处理
O(n),单次子串比较O(1)。
典型题型:
- 子串比较、回文预判、重复串统计。
易错点:
- 哈希不是绝对无冲突,但竞赛里通常可接受。
- 下标建议用
1开始更顺手。
推荐练习:
- 写一道子串相等判断题。
哈希与 KMP 怎么选
什么时候用:
- 一道字符串题既像匹配又像比较时。
核心思路:
- KMP 更擅长模式匹配。
- 哈希更擅长频繁比较子串。
对比表:
| 需求 | 更适合 |
|---|---|
| 找模式串所有出现位置 | KMP |
| 比较多个子串是否相同 | 哈希 |
| 处理字典树式集合问题 | Trie |
| 简单规则模拟 | 直接 string |
推荐练习:
- 复盘两道题,想清楚为什么一题用 KMP、一题用哈希。
回文处理
中心扩展
什么时候用:
- 要找最长回文子串,且数据规模不大或中等。
核心思路:
- 每个位置都可能是回文中心。
- 奇回文和偶回文都要考虑。
模板:
cpp
int ans = 1;
for (int c = 0; c < n; c++) {
for (int l = c, r = c; l >= 0 && r < n && s[l] == s[r]; l--, r++) {
ans = max(ans, r - l + 1);
}
for (int l = c, r = c + 1; l >= 0 && r < n && s[l] == s[r]; l--, r++) {
ans = max(ans, r - l + 1);
}
}
复杂度:
O(n^2)。
典型题型:
- 最长回文子串基础题。
易错点:
- 奇偶回文只写了一种。
推荐练习:
- 做一道基础最长回文题。
Manacher(进阶 / 选学)
什么时候用:
- 最长回文子串,且长度很大,中心扩展可能超时。
核心思路:
- 通过插入分隔符统一奇偶回文。
- 利用已知回文半径加速扩展。
模板:
cpp
string build(const string& s) {
string t = "^";
for (char c : s) {
t += "#";
t += c;
}
t += "#$";
return t;
}
复杂度:
O(n)。
典型题型:
- 大规模回文串。
易错点:
- 赛场上不熟就别硬写。
推荐练习:
- 先理解用途和框架,不一定马上要求手写得飞快。
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 模式串匹配 | KMP |
| 大量字符串插入与查询 | Trie |
| 子串相等判断 | 哈希 |
| 最长回文 | 中心扩展 / Manacher |
| 只涉及简单变换和统计 | string 模拟 |
本章易错点总表
| 错误类型 | 典型错误 | 避免方式 |
|---|---|---|
| KMP 下标错 | j 回退错误 |
先分开写 ne 和匹配 |
| Trie 越界 | 节点数估计太小 | 先估字符串总长度 |
| 哈希边界错 | 前缀哈希下标不统一 | 统一 1 下标 |
| 回文漏情况 | 只处理奇回文 | 奇偶都写 |
| 题型误判 | 简单模拟硬上 KMP | 先判断需求本质 |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 字符串基础模拟 | string 操作 |
题库首页 搜索"字符串" |
| 2 | KMP 入门 | ne 数组和匹配流程 |
题库首页 搜索"KMP" |
| 3 | KMP 变形 | 匹配次数与位置 | 题库首页 搜索"字符串匹配" |
| 4 | Trie | 插入与查询 | 题库首页 搜索"Trie" |
| 5 | 字符串哈希 | 子串比较 | 题库首页 搜索"字符串哈希" |
| 6 | 回文串 | 中心扩展 | 题库首页 搜索"回文" |
| 7 | Manacher 了解 | 线性回文处理 | 题库首页 搜索"Manacher" |
| 8 | 真题字符串题 | 模板综合 | 蓝桥杯真题卷 |
| 9 | 错题分类 | 模拟 / KMP / 哈希区分 | 回看本章 |
| 10 | 手敲模板 | KMP、Trie、哈希连写 | 附录 |
字符串题判断口令
| 如果题目更像这样 | 更可能用什么 |
|---|---|
| 只是统计、替换、判断字符种类 | string 模拟 |
| 在长文本中找模式串 | KMP |
| 比较很多子串是否相等 | 哈希 |
| 维护许多单词和前缀 | Trie |
| 找最长回文子串 | 中心扩展 / Manacher |
字符串常见误区
- 看到"字符串"就条件反射上 KMP。
- 哈希还没想清楚,就直接抄模板。
- 题目明明只需模拟,却把代码写得特别重。
- Trie 的字符集和题目不匹配。
- KMP 的
j指针含义没想清楚。 - 回文题只考虑奇数长度。
getline和cin >> s混用导致读入残缺。- 字符串长度不大却执着上复杂模板。
字符串自测问题
-
- 我能区分字符串模拟和字符串算法题吗?
-
- 我知道什么时候该考虑 KMP 吗?
-
- 我会写 KMP 的
ne数组吗?
- 我会写 KMP 的
-
- 我知道 KMP 成功匹配后为什么还要回退
j吗?
- 我知道 KMP 成功匹配后为什么还要回退
-
- 我会写 Trie 的插入和查询吗?
-
- 我知道 Trie 节点数怎么估吗?
-
- 我会用前缀哈希比较子串吗?
-
- 我知道哈希为什么适合子串比较吗?
-
- 我知道最长回文的两种基础做法吗?
-
- 我能用一句话说明 KMP、Trie、哈希分别解决什么问题吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 字符串题最怕的不是模板本身,而是题型判断失误。
- 先把 KMP 和 Trie 稳住,再补哈希和回文处理,层次会更清楚。
- 如果只想保高频分数,优先顺序可以是:字符串模拟 -> KMP -> Trie -> 哈希。
八、数论与组合数学
本章适合谁
- 数学题看到就发怵,但又知道蓝桥杯里它经常出的人。
- 会写一点
gcd和快速幂,但知识点之间没有串起来的人。 - 想把高频数论和组合数模板补成一套的人。
建议前置知识
- 已掌握前 5 章,尤其是位运算和 DP 基础。
- 会
long long和基本循环。
本章目标
| 板块 | 目标 |
|---|---|
| gcd / lcm | 能处理公约数、公倍数问题 |
| 素数与筛法 | 会判素数、会筛法 |
| 质因数分解 | 会拆分并利用指数信息 |
| 约数 | 会求约数个数与约数和 |
| 快速幂与逆元 | 会做模运算高频题 |
| 欧拉函数与裴蜀定理 | 知道典型用途 |
| 组合数 | 会 Pascal 与阶乘逆元两种主线 |
知识图谱 / 题型雷达
| 题目现象 | 优先联想 |
|---|---|
| 公约数 / 公倍数 | gcd / lcm |
| 判断是否为素数、统计素数 | 判素数 / 筛法 |
| 约数数量和约数和 | 质因数分解 |
a^b mod p |
快速幂 |
| 求模意义下除法 | 逆元 |
| 与互质个数有关 | 欧拉函数 |
ax + by = c |
裴蜀定理 / 扩展欧几里得 |
| 组合数、选法数 | Pascal / 阶乘预处理 |
gcd 与 lcm
欧几里得算法
什么时候用:
- 求两个数的最大公约数。
核心思路:
gcd(a, b) = gcd(b, a % b)。
模板:
cpp
#include <numeric>
long long g = std::gcd(a, b);
复杂度:
O(logn)。
典型题型:
- 分数约分、整除关系、数学构造。
易错点:
- 有人会忘记
a、b为0的边界。
推荐练习:
- 写一个
gcd/lcm基础模板。
最小公倍数
什么时候用:
- 需要多个数共同整除。
核心思路:
lcm(a, b) = a / gcd(a, b) * b。
模板:
cpp
long long l = a / std::gcd(a, b) * b;
复杂度:
- 依赖
gcd,约O(logn)。
典型题型:
- 循环节、步长同步。
易错点:
- 先乘后除容易溢出。
推荐练习:
- 做一道和 gcd / lcm 结合的数列题。
素数与筛法
判素数
什么时候用:
- 只判断单个或少量数字是否为素数。
核心思路:
- 试除到
sqrt(n)即可。
模板:
cpp
bool isPrime(long long x) {
if (x < 2) return false;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) return false;
}
return true;
}
复杂度:
O(sqrt(n))。
典型题型:
- 判素数、少量质因数分析。
易错点:
- 把
1当素数。
推荐练习:
- 写一个判素数函数并测边界值。
埃氏筛与线性筛
什么时候用:
- 需要批量求
1 ~ n的素数。
核心思路:
- 埃氏筛简单好懂。
- 线性筛更稳,且常和后续数论函数一起用。
线性筛模板:
cpp
vector<int> prime;
vector<bool> vis(n + 1, false);
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime.push_back(i);
for (int p : prime) {
if (1LL * i * p > n) break;
vis[i * p] = true;
if (i % p == 0) break;
}
}
复杂度:
- 常说线性筛是
O(n)量级。
典型题型:
- 素数统计、筛法预处理。
易错点:
i * p乘法要防溢出。
推荐练习:
- 写一遍埃氏筛,再写一遍线性筛。
质因数分解
分解一个数
什么时候用:
- 要求某个数的质因子和指数。
核心思路:
- 从小到大试除,每发现一个因子就反复除干净。
模板:
cpp
vector<pair<long long, int>> factorize(long long x) {
vector<pair<long long, int>> res;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
int cnt = 0;
while (x % i == 0) {
x /= i;
cnt++;
}
res.push_back({i, cnt});
}
}
if (x > 1) res.push_back({x, 1});
return res;
}
复杂度:
- 约
O(sqrt(n))。
典型题型:
- 约数、欧拉函数、组合数分解法。
易错点:
- 最后剩下的大素因子忘记加入。
推荐练习:
- 分解若干数字并打印质因子与次数。
约数
约数个数
什么时候用:
- 题目问某个数有多少个正约数。
核心思路:
- 若
n = p1^a1 * p2^a2 * ...,约数个数是(a1 + 1)(a2 + 1)...。
模板:
cpp
long long divisorCount(long long x) {
long long ans = 1;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
int cnt = 0;
while (x % i == 0) {
x /= i;
cnt++;
}
ans *= (cnt + 1);
}
}
if (x > 1) ans *= 2;
return ans;
}
复杂度:
- 依赖质因数分解。
典型题型:
- 数学统计、因子分析。
易错点:
- 忘记剩余大素因子对应的
* 2。
推荐练习:
- 做一道约数个数题。
约数和
什么时候用:
- 题目要所有约数之和。
核心思路:
- 每个质因子的贡献是等比和。
公式:
text
(1 + p + p^2 + ... + p^a)
典型题型:
- 约数和、乘积结构分析。
易错点:
- 把约数个数和约数和公式记混。
推荐练习:
- 先手算几个例子,再实现。
快速幂与逆元
快速幂
什么时候用:
- 求
a^b mod mod。 - 指数很大,不能暴力乘。
核心思路:
- 用二进制拆指数。
模板:
cpp
long long qmi(long long a, long long b, long long mod) {
long long res = 1 % mod;
while (b) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
复杂度:
O(logb)。
典型题型:
- 模幂、逆元、矩阵快速幂前置思想。
易错点:
- 忘记每一步都
% mod。
推荐练习:
- 写一个支持多组查询的快速幂模板。
费马小定理与逆元
什么时候用:
- 模数是素数,且
a与模数互质。
核心思路:
a^(p-2) mod p可作为a的逆元。
模板:
cpp
long long inv(long long a, long long p) {
return qmi(a, p - 2, p);
}
复杂度:
O(logp)。
典型题型:
- 组合数取模、模意义除法。
易错点:
- 不是所有模数都能直接用费马小定理。
推荐练习:
- 做一道模逆元基础题。
欧拉函数与裴蜀定理
欧拉函数
什么时候用:
- 要求与
n互质的正整数个数。
核心思路:
- 边分解质因子,边做
ans = ans / p * (p - 1)。
模板:
cpp
long long phi(long long x) {
long long ans = x;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
ans = ans / i * (i - 1);
while (x % i == 0) x /= i;
}
}
if (x > 1) ans = ans / x * (x - 1);
return ans;
}
复杂度:
- 约
O(sqrt(n))。
典型题型:
- 互质计数、欧拉降幂。
易错点:
- 直接写浮点形式容易出错。
推荐练习:
- 做一道欧拉函数基础题。
裴蜀定理
什么时候用:
- 题目出现
ax + by = c的整数解判定。
核心思路:
ax + by = c有整数解,当且仅当gcd(a, b)整除c。
模板:
cpp
bool hasSolution(long long a, long long b, long long c) {
return c % std::gcd(a, b) == 0;
}
复杂度:
O(logn)。
典型题型:
- 数论构造、线性不定方程判定。
易错点:
- 只会背结论,不会用它判断可行性。
推荐练习:
- 做一道"是否能凑出某个数"的题。
组合数
Pascal 递推
什么时候用:
n不大,直接递推组合数表即可。
核心思路:
C[n][k] = C[n-1][k] + C[n-1][k-1]。
模板:
cpp
for (int i = 0; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
}
复杂度:
O(n^2)。
典型题型:
- 小范围组合数。
易错点:
- 边界
C[i][0]和C[i][i]忘记初始化。
推荐练习:
- 手写杨辉三角并输出前若干行。
阶乘 + 逆元
什么时候用:
- 需要多次查询组合数,且模数常为大质数。
核心思路:
- 预处理阶乘和逆阶乘。
模板:
cpp
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mod;
ifac[n] = qmi(fac[n], mod - 2, mod);
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % mod;
auto C = [&](int n, int m) -> long long {
if (m < 0 || m > n) return 0;
return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
};
复杂度:
- 预处理
O(n),单次查询O(1)。
典型题型:
- 多组组合数查询、计数 DP。
易错点:
- 模数不是质数时,逆元方案要重新判断。
推荐练习:
- 做一道多次询问组合数题。
模运算常见技巧
防止负数模问题
什么时候用:
- 减法取模。
核心思路:
- 统一写成
(x % mod + mod) % mod或x = (x + mod) % mod。
模板:
cpp
x = (x - y) % mod;
if (x < 0) x += mod;
复杂度:
- 常数。
典型题型:
- 快速幂、组合数、前缀计数取模。
易错点:
- C++ 中负数
%可能不是你直觉里的正值。
推荐练习:
- 专门检查自己模运算里的减法。
本章常见题型识别
| 题型现象 | 首选思路 |
|---|---|
| 公约数 / 公倍数 | gcd / lcm |
| 判素数、筛素数 | 试除 / 筛法 |
| 求约数个数或约数和 | 质因数分解 |
| 幂很大 | 快速幂 |
| 模意义下除法 | 逆元 |
| 组合数多次查询 | 阶乘 + 逆元 |
本章易错点总表
| 错误类型 | 典型错误 | 避免方式 |
|---|---|---|
| 把 1 当素数 | 判素数边界没处理 | 明确 x < 2 返回假 |
| lcm 溢出 | 先乘后除 | 先除后乘 |
| 快速幂漏取模 | 中间结果爆掉 | 每次乘法后 % mod |
| 逆元乱用 | 模数不是素数还用费马 | 先判断条件 |
| 组合数初始化错 | 阶乘或逆阶乘边界没设 | 固定模板 |
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | gcd / lcm | 欧几里得算法 | 题库首页 搜索"gcd" |
| 2 | 判素数 | 试除法边界 | 题库首页 搜索"素数" |
| 3 | 筛法 | 批量素数预处理 | 题库首页 搜索"筛法" |
| 4 | 质因数分解 | 指数统计 | 题库首页 搜索"质因数分解" |
| 5 | 约数个数 / 和 | 公式应用 | 题库首页 搜索"约数" |
| 6 | 快速幂 | 模运算主模板 | 题库首页 搜索"快速幂" |
| 7 | 逆元 / 费马 | 模意义除法 | 题库首页 搜索"逆元" |
| 8 | 欧拉函数 / 裴蜀定理 | 中档数论 | 题库首页 搜索"欧拉函数" 或 "裴蜀定理" |
| 9 | 组合数 | Pascal 与阶乘预处理 | 题库首页 搜索"组合数" |
| 10 | 真题数学题 | 模板综合 | 蓝桥杯真题卷 |
数论模板选型表
| 问题关键词 | 优先模板 | 备注 |
|---|---|---|
| 最大公约数 | gcd |
最基础 |
| 最小公倍数 | lcm |
先除后乘 |
| 判一个数是不是素数 | 试除法 | 枚举到 sqrt(n) |
| 批量求素数 | 筛法 | 埃氏筛 / 线性筛 |
| 求质因子与次数 | 质因数分解 | 为约数与欧拉函数服务 |
a^b mod p |
快速幂 | 高频 |
| 模意义下除法 | 逆元 | 先看模数条件 |
| 与互质个数有关 | 欧拉函数 | 中档数论 |
ax + by = c |
裴蜀定理 | 先判整除条件 |
| 大量组合数查询 | 阶乘 + 逆元 | 模数常为素数 |
组合数方案选择表
| 场景 | 更适合的方法 |
|---|---|
n 不大 |
Pascal 递推 |
| 查询很多次 | 阶乘 + 逆元 |
| 只是少量单次求值 | 直接公式或递推 |
| 模数是大质数 | 阶乘 + 逆元很常见 |
数论自测问题
-
- 我知道
1不是素数吗?
- 我知道
-
- 我会写
gcd和lcm吗?
- 我会写
-
- 我知道
lcm为什么要先除后乘吗?
- 我知道
-
- 我会写判素数吗?
-
- 我会写筛法吗?
-
- 我知道什么时候筛法比试除法更合适吗?
-
- 我会写质因数分解吗?
-
- 我知道约数个数公式吗?
-
- 我知道约数和公式的思路吗?
-
- 我会写快速幂吗?
-
- 我知道逆元常见适用条件吗?
-
- 我会写欧拉函数吗?
-
- 我知道裴蜀定理判断可行性的条件吗?
-
- 我会用 Pascal 递推求组合数吗?
-
- 我会写阶乘 + 逆元求组合数吗?
-
- 我知道取模减法为什么要防负数吗?
-
- 我知道哪些数学题其实只是模板题吗?
-
- 我有把数论题按模板归类吗?
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 数论并不一定比图论更难,很多时候只是模板和判定条件不熟。
- 对蓝桥杯来说,先把 gcd、筛法、快速幂、逆元、组合数这条线打牢,收益非常高。
- 遇到数学题时不要慌,先判断它属于哪个模板族。
九、真题题型路线与备赛策略
本章适合谁
- 不只是想"学会知识点",还想知道"该怎么练"的同学。
- 已经学过一些专题,但不知道先刷什么、后刷什么的人。
- 想把蓝桥杯备赛流程做得更稳定、更体系化的人。
本章怎么用
- 如果你还没系统学专题:把本章当路线图。
- 如果你已经会很多模板:把本章当训练计划和冲刺清单。
- 如果你马上要比赛:重点看"赛前 7 天 / 3 天 / 1 天计划"和"比赛当天流程"。
官方资源怎么用
官方入口 1:蓝桥云课题库
链接:
适合做什么:
- 按关键词搜索专题题。
- 按模块补短板。
- 做基础题和中档题。
怎么用更有效:
- 先在本章确定你当前最缺哪一类题。
- 再去题库按专题关键词搜索,而不是盲目乱刷。
- 每做完一个专题,至少整理 3 个固定模板。
官方入口 2:蓝桥杯真题卷
链接:
适合做什么:
- 模拟真实比赛。
- 看高频题型在真题里的出现方式。
- 训练时间分配和查错节奏。
怎么用更有效:
- 平时可以拆题做。
- 冲刺阶段要整卷限时做。
- 做完后不要只看分数,一定要记录失分原因。
官方入口 3:历届真题课程
链接:
适合做什么:
- 看官方讲解入口。
- 回顾历届典型题型。
怎么用更有效:
- 用来配合你已经做过的题,不建议一上来只看讲解不自己做。
先评估自己在哪个阶段
| 阶段 | 典型表现 | 优先任务 |
|---|---|---|
| 入门阶段 | STL 不熟,基础题也常写挂 | 先补第 1、2、3 章 |
| 进阶阶段 | 基础题能做,中档题容易卡 | 补第 4、5、7、8 章 |
| 冲刺阶段 | 大部分专题见过,但不稳定 | 补第 6 章并开始真题整卷 |
| 提分阶段 | 会做不少题,但比赛波动大 | 强化第 9 章策略与附录模板压缩 |
高频题型映射表
| 题面信号 | 对应专题 | 首选章节 |
|---|---|---|
| 多次区间和 | 前缀和 | 第 3 章 |
| 多次区间修改 | 差分 | 第 3 章 |
| 连续区间最短 / 最长 | 双指针 / 滑窗 | 第 3 章 |
| 需要最少步数 | BFS | 第 4 章 |
| 枚举所有方案 | DFS / 回溯 | 第 4 章 |
| 最优值带前缀结构 | DP | 第 5 章 |
| 选物品受容量限制 | 背包 | 第 5 章 |
| 图连通性 | 并查集 / DFS / BFS | 第 6 章 |
| 带权最短路 | Dijkstra | 第 6 章 |
| 先后依赖 | 拓扑排序 | 第 6 章 |
| 模式串匹配 | KMP | 第 7 章 |
| 大量字符串查询 | Trie | 第 7 章 |
| 子串比较 | 哈希 | 第 7 章 |
| 公约数 / 公倍数 | gcd / lcm | 第 8 章 |
| 素数、筛法 | 数论 | 第 8 章 |
| 模幂 | 快速幂 | 第 8 章 |
| 模意义组合数 | 逆元 + 组合数 | 第 8 章 |
常见失分原因映射表
| 失分现象 | 深层原因 | 该回看哪章 |
|---|---|---|
| 样例过了但 WA | 边界、初始化、条件不全 | 第 1、2、9 章 |
| TLE | 复杂度判断失误 | 第 2 章 |
| 前缀和写炸 | 下标体系混乱 | 第 3 章 |
| 搜索爆炸 | 没剪枝或状态重复 | 第 4 章 |
| DP 不会下手 | 状态定义不清 | 第 5 章 |
| 图题建边就乱 | 图存储不熟 | 第 6 章 |
| 字符串看不出题型 | 模板判断没形成体系 | 第 7 章 |
| 数学题一脸空白 | 模板族没有归类 | 第 8 章 |
训练路线一:从零散补漏到专题成型
第 1 阶段:语言与实现稳定
目标:
- 确保不再因为 STL、排序、输入输出、类型问题吃亏。
重点章节:
- 第 1 章
- 第 2 章
- 第 3 章
训练方式:
- 每天 2 到 3 道基础题。
- 每天手敲 2 个模板。
- 每天复盘 1 次错误原因。
第 2 阶段:搜索与 DP 建核心能力
目标:
- 让你能解决大部分中档题。
重点章节:
- 第 4 章
- 第 5 章
- 第 7 章
- 第 8 章
训练方式:
- 每天 1 个专题。
- 每个专题做 3 到 5 道题。
- 记录每道题属于哪一种模型。
第 3 阶段:中高频专题补缺
目标:
- 把图论、并查集、单调结构、树状数组等补上。
重点章节:
- 第 6 章
训练方式:
- 先保高频:并查集、 Dijkstra、拓扑、单调栈。
- 树状数组和线段树按时间决定深度。
第 4 阶段:真题串联
目标:
- 从"会做专题题"转向"会做比赛题"。
训练方式:
- 用官方真题卷整卷模拟。
- 记录每题属于哪个专题和哪个错误类型。
- 把做错原因回链到章节复习。
训练路线二:7 天冲刺计划
第 1 天
目标:
- 语言与高频模板回温。
任务:
- 第 1 章通读。
- 第 3 章前缀和 / 差分 / 双指针重写一遍。
- 附录里把比赛模板、二分、前缀和手敲一遍。
第 2 天
目标:
- 搜索题稳定。
任务:
- 第 4 章 DFS / BFS / 回溯。
- 练 4 道搜索题:排列、连通块、网格 BFS、记忆化搜索各一题。
第 3 天
目标:
- DP 主线过一遍。
任务:
- 第 5 章线性 DP、背包、网格 DP。
- 手敲 0-1 背包和 LIS。
第 4 天
目标:
- 图论与数据结构补高频。
任务:
- 第 6 章并查集、拓扑、 Dijkstra、单调栈。
- 每块各做 1 题。
第 5 天
目标:
- 字符串与数论补模板。
任务:
- 第 7 章 KMP、Trie、哈希。
- 第 8 章 gcd、筛法、快速幂、组合数。
第 6 天
目标:
- 做一套官方真题卷。
任务:
- 限时模拟。
- 做完后按"思路错 / 细节错 / 模板不会 / 时间分配错"分类。
第 7 天
目标:
- 模板压缩与心态稳定。
任务:
- 看附录。
- 看本章比赛当天流程。
- 只做轻量复盘,不再盲目开新题。
训练路线三:长期系统学习计划
| 周次 | 重点内容 | 建议成果 |
|---|---|---|
| 第 1 周 | 第 1 章 + 第 2 章 | 排序、二分、模拟稳定 |
| 第 2 周 | 第 3 章 | 高频模板成体系 |
| 第 3 周 | 第 4 章 | DFS / BFS 稳定 |
| 第 4 周 | 第 5 章上半 | 线性 DP、背包 |
| 第 5 周 | 第 5 章下半 | 序列 DP、区间 DP 入门 |
| 第 6 周 | 第 6 章上半 | 并查集、拓扑、最短路 |
| 第 7 周 | 第 6 章下半 | 单调结构、树状数组、线段树 |
| 第 8 周 | 第 7 章 + 第 8 章 | 字符串与数论补齐 |
| 第 9 周起 | 第 9 章 | 真题串联与模拟比赛 |
题型优先级建议
B 组主线优先级
- 第 1 章:基础与 STL
- 第 3 章:前缀和、差分、双指针
- 第 4 章:搜索
- 第 5 章:线性 DP、背包
- 第 8 章:gcd、筛法、快速幂
- 第 7 章:KMP、Trie
- 第 6 章:并查集、 Dijkstra、拓扑
A 组补充优先级
- 第 6 章:树状数组、线段树
- 第 5 章:区间 DP、状态压缩 DP
- 第 7 章:Manacher
- 第 8 章:更系统的组合数与数论技巧
分能力层练题建议
只会基础语法的同学
先做这些类型:
- 排序
- 模拟
- 前缀和
- 差分
- 双指针基础
- DFS / BFS 基础
暂时少碰:
- 线段树
- 状态压缩 DP
- 难图论综合题
会基础算法但不稳定的同学
重点做这些:
- 搜索与 DP
- 字符串模板
- 数论高频
- 并查集与 Dijkstra
已经会很多题但比赛波动大的同学
重点练这些:
- 真题整卷
- 限时写题
- 查错速度
- 模板压缩
- 赛场策略
真题训练的三种方式
方式一:按专题拆卷
适合谁:
- 还在补专题阶段的人。
怎么做:
- 从真题卷里挑出同类题,按专题集中刷。
优点:
- 容易建立模型感。
缺点:
- 训练不到整场节奏。
方式二:按难度分层做
适合谁:
- 想稳步提分的人。
怎么做:
- 先做你最有把握的题型。
- 再逐渐加中档题。
优点:
- 挫败感少,容易建立信心。
缺点:
- 容易在舒适区停留。
方式三:整卷限时模拟
适合谁:
- 冲刺阶段。
怎么做:
- 找安静时间,完整模拟比赛。
- 严格控制时间,不随便暂停。
优点:
- 最接近实战。
缺点:
- 复盘成本高,但值得。
赛前 30 天计划
| 天数 | 重点内容 | 任务重点 |
|---|---|---|
| Day 1 | 第 1 章 | 竞赛模板、 STL、排序、二分函数 |
| Day 2 | 第 2 章 | 枚举与模拟 |
| Day 3 | 第 2 章 | 二分查找与二分答案 |
| Day 4 | 第 3 章 | 一维前缀和、差分 |
| Day 5 | 第 3 章 | 二维前缀和、双指针 |
| Day 6 | 第 3 章 | 位运算、离散化 |
| Day 7 | 复盘 | 回看错题和模板 |
| Day 8 | 第 4 章 | 递归、排列回溯 |
| Day 9 | 第 4 章 | 组合、子集、剪枝 |
| Day 10 | 第 4 章 | 网格 DFS / BFS |
| Day 11 | 第 4 章 | 记忆化搜索 |
| Day 12 | 第 5 章 | 线性 DP |
| Day 13 | 第 5 章 | LIS / LCS |
| Day 14 | 第 5 章 | 0-1 背包 |
| Day 15 | 第 5 章 | 完全背包 / 多重背包 |
| Day 16 | 第 5 章 | 网格 DP |
| Day 17 | 第 5 章 | 区间 DP |
| Day 18 | 第 6 章 | 建图、并查集 |
| Day 19 | 第 6 章 | 拓扑排序 |
| Day 20 | 第 6 章 | Dijkstra |
| Day 21 | 第 6 章 | Kruskal |
| Day 22 | 第 6 章 | 单调栈 / 单调队列 |
| Day 23 | 第 6 章 | 树状数组 |
| Day 24 | 第 6 章 | 线段树入门 |
| Day 25 | 第 7 章 | KMP |
| Day 26 | 第 7 章 | Trie、哈希 |
| Day 27 | 第 8 章 | gcd、筛法、分解 |
| Day 28 | 第 8 章 | 快速幂、逆元、组合数 |
| Day 29 | 真题整卷 | 限时模拟 |
| Day 30 | 总复盘 | 只看错题、附录和本章 |
赛前 14 天计划
| 天数 | 重点内容 | 每天输出 |
|---|---|---|
| Day 1 | 第 1 章 + 附录 | 语言模板复习表 |
| Day 2 | 第 3 章 | 前缀和 / 差分模板手敲 |
| Day 3 | 第 3 章 | 双指针 / 位运算模板手敲 |
| Day 4 | 第 4 章 | DFS / BFS 模板复习 |
| Day 5 | 第 5 章 | 线性 DP / 背包 |
| Day 6 | 第 6 章 | 并查集 / Dijkstra |
| Day 7 | 第 7 章 | KMP / Trie |
| Day 8 | 第 8 章 | gcd / 快速幂 / 组合数 |
| Day 9 | 真题专题练 | 按弱项刷题 |
| Day 10 | 真题专题练 | 按弱项刷题 |
| Day 11 | 真题整卷 | 限时 1 套 |
| Day 12 | 复盘 | 错题整理 |
| Day 13 | 再模拟 | 限时 1 套 |
| Day 14 | 压模板 | 附录与检查清单 |
赛前 3 天计划
| 时间 | 重点任务 |
|---|---|
| 第 1 天上午 | 看附录与第 3 章 |
| 第 1 天下午 | 看第 4 章与第 5 章高频 |
| 第 2 天上午 | 看第 6 章高频与第 7 章 |
| 第 2 天下午 | 看第 8 章与错题本 |
| 第 3 天上午 | 做一套轻量真题或旧题回顾 |
| 第 3 天下午 | 不开新题,只压模板和检查清单 |
赛前 1 天计划
该做什么
- 看附录模板。
- 看自己最容易错的 10 个点。
- 做少量熟悉题,保持手感。
- 整理比赛输入输出、数组大小、模板风格。
不该做什么
- 不临时硬学很新的重型算法。
- 不大量做陌生难题把心态做崩。
- 不到处看零碎技巧帖导致脑子更乱。
比赛当天流程
开赛前 10 分钟
- 检查编译环境。
- 准备好最顺手的模板。
- 深呼吸,不要临时改风格。
开赛后前 10 分钟
- 快速浏览全卷。
- 先判断哪题最稳。
- 先做能拿下的,不要在第一题卡太久。
开赛后前 60 分钟
- 优先拿基础分和中等稳定分。
- 能一遍写对的题优先。
- 写完一题先自己过边界再提交。
中段阶段
- 如果卡住超过预设时间,先跳。
- 把状态记在草稿里,不要反复从头读题。
最后 30 分钟
- 不要乱开新大题。
- 优先回头查已写代码的边界、数组、类型。
- 看附录里的最后检查清单。
时间分配建议
| 情况 | 建议做法 |
|---|---|
| 第一题卡住 | 5 到 10 分钟没进展就看下一题 |
| 中档题有思路但实现繁琐 | 先写伪代码,再决定是否开做 |
| 只剩 20 分钟 | 优先检查已写题,而不是开陌生大题 |
| 已经拿下多题 | 稳住心态,别因为贪难题把已得分写挂 |
赛场查错顺序
- 数组大小和下标。
int/long long。sort和二分区间。- 初始化是否漏掉。
- 多组数据是否清空。
- BFS / DFS 是否重复访问。
- DP 初值和答案位置。
- 模运算是否每步取模。
真题复盘模板
每做完一套卷,至少记录以下内容:
| 记录项 | 你要写什么 |
|---|---|
| 会做但写挂的题 | 错在边界、类型、初始化还是实现顺序 |
| 完全没思路的题 | 属于哪个专题,为什么没想到 |
| 花时间过长的题 | 卡在哪一步 |
| 提交后才发现的问题 | 为什么没在本地检查出来 |
| 下次行动 | 回看哪章、补哪类题、背哪个模板 |
错题分类法
类型一:思路型错误
表现:
- 读完题不知道从哪里下手。
说明:
- 通常是题型识别不到位。
应对:
- 回看专题章节里的"题型识别"。
类型二:模板型错误
表现:
- 想到了 KMP、 Dijkstra、背包,但写不顺。
说明:
- 模板不熟。
应对:
- 去附录手敲模板。
类型三:实现型错误
表现:
- 思路和模板都对,但 WA / RE。
说明:
- 边界、初始化、细节控制不稳。
应对:
- 对照本章查错顺序。
类型四:比赛型错误
表现:
- 会做的题没拿到,会做但来不及写完。
说明:
- 时间分配和心态管理有问题。
应对:
- 多做整卷模拟,形成自己的比赛节奏。
章节与训练任务对应表
| 章节 | 最低目标 | 进阶目标 | 冲刺目标 |
|---|---|---|---|
| 第 1 章 | 常用 STL 会用 | 排序 + 二分函数顺手 | 基础实现不丢分 |
| 第 2 章 | 看得出枚举 / 模拟 / 二分 | 会优化复杂度 | 读题阶段快速定位 |
| 第 3 章 | 前缀和、差分、双指针会写 | 位运算和离散化会用 | 高频模板秒切 |
| 第 4 章 | DFS / BFS 会写 | 剪枝和记忆化更稳 | 搜索题稳定拿分 |
| 第 5 章 | 线性 DP、背包会写 | LIS / LCS / 区间 DP | 中档 DP 题不怕 |
| 第 6 章 | 并查集、 Dijkstra 会写 | 单调结构和树状数组 | 中高频题稳住 |
| 第 7 章 | KMP、 Trie 基础 | 哈希和回文 | 字符串题不乱判 |
| 第 8 章 | gcd、筛法、快速幂 | 组合数和欧拉函数 | 数学题有模板感 |
按专题搜索关键词清单
| 专题 | 题库搜索关键词 |
|---|---|
| 排序 | 排序、区间排序、自定义排序 |
| 二分 | 二分、二分答案、最小最大值 |
| 前缀和 | 前缀和、区间和、子矩阵和 |
| 差分 | 差分、区间修改 |
| 双指针 | 双指针、滑动窗口、连续子数组 |
| 位运算 | 位运算、状态压缩、 lowbit |
| DFS | 深搜、回溯、排列、组合 |
| BFS | 广搜、最短步数、迷宫 |
| 记忆化搜索 | 记忆化搜索、搜索优化 |
| 线性 DP | 动态规划、线性 DP |
| 背包 | 0-1 背包、完全背包、多重背包 |
| 序列 DP | LIS、LCS、最长上升子序列 |
| 图论 | 图、最短路、并查集、拓扑排序 |
| 单调结构 | 单调栈、单调队列 |
| 字符串 | 字符串、 KMP、 Trie、哈希 |
| 数论 | gcd、素数、筛法、快速幂、组合数 |
训练记录建议
每天训练结束后,建议至少记 5 件事:
- 今天做了哪些题。
- 哪一题最值得复盘。
- 今天学会了哪个模板。
- 今天最容易犯的错误是什么。
- 明天应该优先补什么。
如果时间不够怎么办
只剩 7 天
- 保住第 1、3、4、5、8 章主线。
- 第 6 章只看并查集、 Dijkstra、拓扑。
- 第 7 章只保 KMP 和 Trie。
只剩 3 天
- 高强度看附录。
- 真题只做熟题型,别乱开重题。
- 重点查自己最容易错的 20 个细节。
只剩 1 天
- 停止扩充知识面。
- 只做熟悉和提神的内容。
- 把心态和稳定性放第一位。
本章练习路线
| 顺序 | 练习方向 | 核心训练点 | 官方入口建议 |
|---|---|---|---|
| 1 | 专题基础题 | 建立模块感 | 题库首页 |
| 2 | 高频模板题 | 形成手感 | 题库首页 按关键词搜 |
| 3 | 中档专题题 | 题型识别 | 蓝桥杯真题卷 |
| 4 | 弱项专题强化 | 定向补短板 | 回看对应章节 |
| 5 | 真题拆卷 | 按专题刷真题 | 蓝桥杯真题卷 |
| 6 | 真题整卷 | 训练比赛节奏 | 蓝桥杯真题卷 |
| 7 | 错题二刷 | 检查是否真补上 | 自己整理的错题本 |
| 8 | 模板压缩 | 不看资料手敲 | 附录 |
| 9 | 考前轻量复盘 | 稳心态与查漏 | 本章 + 附录 |
| 10 | 比赛当天执行 | 按流程做事 | 本章比赛流程 |
30 题专题冲刺清单
| 题号 | 练习主题 | 目标 |
|---|---|---|
| 1 | 排序基础 | 热手和稳定实现 |
| 2 | 自定义排序 | 多关键字判断 |
| 3 | 二分查找 | 边界稳定 |
| 4 | 二分答案 | check 思维 |
| 5 | 一维前缀和 | 区间和 |
| 6 | 二维前缀和 | 容斥 |
| 7 | 一维差分 | 区间修改 |
| 8 | 双指针 | 连续区间 |
| 9 | 位运算 | 位判断和 lowbit |
| 10 | 排列回溯 | 搜索入门 |
| 11 | 组合回溯 | 搜索范围控制 |
| 12 | 网格 DFS | 连通块 |
| 13 | 网格 BFS | 最短步数 |
| 14 | 记忆化搜索 | 降低重复状态 |
| 15 | 线性 DP | 最优值转移 |
| 16 | LIS | 序列 DP |
| 17 | 0-1 背包 | 经典模型 |
| 18 | 完全背包 | 循环方向 |
| 19 | 网格 DP | 路径最值 |
| 20 | 并查集 | 连通性 |
| 21 | 拓扑排序 | 依赖关系 |
| 22 | Dijkstra | 非负最短路 |
| 23 | Kruskal | 最小生成树 |
| 24 | 单调栈 | 最近更大 / 更小 |
| 25 | 单调队列 | 滑窗最值 |
| 26 | KMP | 模式匹配 |
| 27 | Trie | 字符串集合 |
| 28 | gcd / 筛法 | 数论基础 |
| 29 | 快速幂 | 模板必会 |
| 30 | 组合数 | 数学高频 |
模拟赛记录卡
每做完一次模拟,建议记录:
- 本次用时。
- 第一道 AC 的题。
- 花时间最长的题。
- 最可惜的一道题。
- 最值得回看的一个错误。
- 下次模拟前最该补的专题。
赛后升级动作
- 做对的题:压缩模板,提升速度。
- 想到但没写完的题:补实现稳定性。
- 完全没想到的题:定位对应章节。
- 写挂的题:记录错误类别。
- 超时的题:回看复杂度判断。
- 提交后才发现的 bug:纳入最后检查清单。
训练执行提醒
- 不要同时补太多弱项,先抓最影响得分的 1 到 2 个专题。
- 真题整卷后一定要复盘,不复盘等于白做一半。
- 冲刺阶段不要盲目扩知识面,稳定性比新知识更重要。
- 每周至少有一次"完全不看资料手敲模板"的训练。
- 错题本最好按章节和错误类型双维度整理。
资料延伸区
官方练习
算法阅读
接口查阅
本章收尾建议
- 真题训练的目的不是"把题全刷完",而是建立自己的得分顺序和查错节奏。
- 一套资料真正有用,不只是让你知道知识点,还要让你知道什么时候练、怎么练、错了怎么补。
- 当你开始把"题型 -> 专题 -> 模板 -> 易错点 -> 复盘动作"串起来时,备赛就进入正轨了。
十、模板速查手册
这一份附录给你两个用途:
- 平时训练时快速回忆模板。
- 比赛前 1 天压缩记忆。
真正想把模板吃透,最好的方式不是反复看,而是反复手敲。
速查目录
- 比赛起手模板
- 排序、二分、去重、离散化
- 前缀和与差分
- 双指针与位运算
- 搜索模板
- 动态规划模板
- 图论与数据结构模板
- 字符串模板
- 数论与组合数模板
- 复杂度、公式与检查清单
一、比赛起手模板
基础模板
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using pii = pair<int, int>;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3fLL;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
return 0;
}
适用场景:
- 几乎所有普通题。
提醒:
long long常量记得带LL。- 输入量大时默认加速。
带全局数组模板
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int N = 200000 + 5;
int a[N];
ll s[N];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
return 0;
}
适用场景:
- 搜索、图论、前缀和、 DP。
提醒:
- 数组大小尽量多留余量。
二、排序、二分、去重、离散化
sort
cpp
sort(a + 1, a + n + 1);
sort(v.begin(), v.end());
复杂度:
O(nlogn)。
提醒:
- 右端点是开区间。
自定义排序
cpp
struct Node {
int a, b;
};
bool cmp(const Node& x, const Node& y) {
if (x.a != y.a) return x.a < y.a;
return x.b > y.b;
}
sort(v.begin(), v.end(), cmp);
适用场景:
- 多关键字排序。
提醒:
- 关键字顺序别写反。
lower_bound / upper_bound
cpp
int pos1 = lower_bound(a + 1, a + n + 1, x) - a;
int pos2 = upper_bound(a + 1, a + n + 1, x) - a;
记忆:
lower_bound:第一个大于等于xupper_bound:第一个大于x
基础二分
cpp
int l = 0, r = n - 1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] == target) {
break;
} else if (a[mid] < target) {
l = mid + 1;
} else {
r = mid - 1;
}
}
左边界二分
cpp
int l = 0, r = n - 1, ans = n;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] >= target) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
右边界二分
cpp
int l = 0, r = n - 1, ans = -1;
while (l <= r) {
int mid = l + (r - l) / 2;
if (a[mid] <= target) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
二分答案
cpp
bool check(long long mid) {
// 判断 mid 是否可行
}
long long l = 0, r = (long long)1e18, ans = -1;
while (l <= r) {
long long mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
识别信号:
- 最小的最大值
- 最大的最小值
- 至少 / 至多 / 不超过
浮点二分
cpp
double l = 0, r = 1e6;
for (int i = 0; i < 100; i++) {
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
提醒:
- 浮点二分更常看精度,不一定用
while。
去重
cpp
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());
离散化
cpp
vector<int> alls = a;
sort(alls.begin(), alls.end());
alls.erase(unique(alls.begin(), alls.end()), alls.end());
auto getId = [&](int x) {
return lower_bound(alls.begin(), alls.end(), x) - alls.begin() + 1;
};
提醒:
- 记得把查询值和端点值也收集进去。
三、前缀和与差分
一维前缀和
cpp
vector<long long> s(n + 1, 0);
for (int i = 1; i <= n; i++) {
s[i] = s[i - 1] + a[i];
}
auto rangeSum = [&](int l, int r) {
return s[r] - s[l - 1];
};
二维前缀和
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = sum[i - 1][j] + sum[i][j - 1] - sum[i - 1][j - 1] + a[i][j];
}
}
auto get = [&](int x1, int y1, int x2, int y2) {
return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
};
一维差分
cpp
vector<long long> diff(n + 2, 0);
for (int i = 1; i <= m; i++) {
int l, r;
long long c;
cin >> l >> r >> c;
diff[l] += c;
diff[r + 1] -= c;
}
for (int i = 1; i <= n; i++) {
diff[i] += diff[i - 1];
a[i] += diff[i];
}
二维差分
cpp
auto add = [&](int x1, int y1, int x2, int y2, long long c) {
d[x1][y1] += c;
d[x2 + 1][y1] -= c;
d[x1][y2 + 1] -= c;
d[x2 + 1][y2 + 1] += c;
};
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
d[i][j] += d[i - 1][j] + d[i][j - 1] - d[i - 1][j - 1];
}
}
前缀计数
cpp
vector<int> cnt(n + 1, 0);
for (int i = 1; i <= n; i++) {
cnt[i] = cnt[i - 1] + (a[i] == target);
}
前缀异或
cpp
vector<int> pre(n + 1, 0);
for (int i = 1; i <= n; i++) {
pre[i] = pre[i - 1] ^ a[i];
}
提醒:
- 前缀和数组一般优先开
long long。 - 差分数组要多开一位。
四、双指针与位运算
对撞指针
cpp
sort(a.begin(), a.end());
int l = 0, r = (int)a.size() - 1;
while (l < r) {
long long sum = 1LL * a[l] + a[r];
if (sum == target) break;
if (sum < target) l++;
else r--;
}
快慢指针去重
cpp
sort(a.begin(), a.end());
int j = 0;
for (int i = 0; i < (int)a.size(); i++) {
if (i == 0 || a[i] != a[i - 1]) {
a[j++] = a[i];
}
}
滑动窗口
cpp
int l = 0;
for (int r = 0; r < n; r++) {
// 加入 a[r]
while (窗口不合法) {
// 移出 a[l]
l++;
}
// 更新答案
}
位判断
cpp
bool isOdd = (x & 1);
bool bit = ((x >> k) & 1);
位修改
cpp
x |= (1LL << k);
x &= ~(1LL << k);
x ^= (1LL << k);
lowbit
cpp
int lowbit(int x) {
return x & -x;
}
枚举所有子集
cpp
for (int mask = 0; mask < (1 << n); mask++) {
// mask 是一个子集
}
枚举非空子集
cpp
for (int s = mask; s; s = (s - 1) & mask) {
// s 是 mask 的非空子集
}
提醒:
1 << 40要写成1LL << 40。- 滑动窗口最关键的是"何时收缩"和"何时更新答案"。
五、搜索模板
排列回溯
cpp
int n;
vector<int> path;
bool used[25];
void dfs(int step) {
if (step == n) {
return;
}
for (int i = 1; i <= n; i++) {
if (used[i]) continue;
used[i] = true;
path.push_back(i);
dfs(step + 1);
path.pop_back();
used[i] = false;
}
}
组合回溯
cpp
int n, k;
vector<int> path;
void dfs(int start) {
if ((int)path.size() == k) {
return;
}
for (int i = start; i <= n; i++) {
path.push_back(i);
dfs(i + 1);
path.pop_back();
}
}
子集回溯
cpp
void dfs(int u) {
if (u > n) {
return;
}
dfs(u + 1);
path.push_back(u);
dfs(u + 1);
path.pop_back();
}
网格 DFS
cpp
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
void dfs(int x, int y) {
vis[x][y] = true;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (vis[nx][ny] || g[nx][ny] == '#') continue;
dfs(nx, ny);
}
}
网格 BFS
cpp
queue<pair<int, int>> q;
memset(dist, -1, sizeof dist);
dist[sx][sy] = 0;
q.push({sx, sy});
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (g[nx][ny] == '#') continue;
if (dist[nx][ny] != -1) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
}
}
图 BFS
cpp
vector<int> dist(n + 1, -1);
queue<int> q;
dist[s] = 0;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (dist[v] != -1) continue;
dist[v] = dist[u] + 1;
q.push(v);
}
}
多源 BFS
cpp
for (auto [x, y] : sources) {
dist[x][y] = 0;
q.push({x, y});
}
记忆化搜索
cpp
vector<int> dp(n, -1);
int dfs(int x) {
if (dp[x] != -1) return dp[x];
int res = 1;
for (int y = x + 1; y < n; y++) {
if (a[y] > a[x]) {
res = max(res, dfs(y) + 1);
}
}
return dp[x] = res;
}
提醒:
- 回溯固定结构:做选择 -> 递归 -> 撤销选择。
- BFS 一般"入队就标记"更稳。
六、动态规划模板
一维线性 DP
cpp
vector<long long> dp(n + 1, 0);
dp[0] = 1;
for (int i = 1; i <= n; i++) {
dp[i] = dp[i - 1];
}
LIS
cpp
vector<int> dp(n, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
int ans = *max_element(dp.begin(), dp.end());
LCS
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
if (a[i] == b[j]) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1);
}
}
}
0-1 背包
cpp
vector<long long> dp(V + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = V; j >= v[i]; j--) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
完全背包
cpp
vector<long long> dp(V + 1, 0);
for (int i = 1; i <= n; i++) {
for (int j = v[i]; j <= V; j++) {
dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
}
}
网格 DP
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + a[i][j];
}
}
区间 DP
cpp
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = l + len - 1;
dp[l][r] = INF;
for (int k = l; k < r; k++) {
dp[l][r] = min(dp[l][r], dp[l][k] + dp[k + 1][r] + cost(l, r));
}
}
}
状态压缩 DP 框架
cpp
for (int mask = 0; mask < (1 << n); mask++) {
for (int i = 0; i < n; i++) {
if ((mask >> i) & 1) {
// 转移
}
}
}
提醒:
- 0-1 背包逆序。
- 完全背包正序。
- DP 先写中文状态定义再写代码。
七、图论与数据结构模板
邻接表
cpp
vector<vector<int>> g(n + 1);
for (int i = 0; i < m; i++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
并查集
cpp
vector<int> fa(n + 1);
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void merge(int a, int b) {
a = find(a);
b = find(b);
if (a != b) fa[a] = b;
}
拓扑排序
cpp
vector<int> deg(n + 1, 0), order;
queue<int> q;
for (int i = 1; i <= n; i++) {
if (deg[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : g[u]) {
if (--deg[v] == 0) q.push(v);
}
}
Dijkstra
cpp
using PII = pair<long long, int>;
vector<long long> dist(n + 1, (long long)4e18);
vector<bool> vis(n + 1, false);
priority_queue<PII, vector<PII>, greater<PII>> pq;
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (auto [v, w] : g[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
Kruskal
cpp
struct Edge {
int u, v, w;
bool operator < (const Edge& other) const {
return w < other.w;
}
};
sort(edges.begin(), edges.end());
long long ans = 0;
int cnt = 0;
for (auto &e : edges) {
int fu = find(e.u), fv = find(e.v);
if (fu == fv) continue;
fa[fu] = fv;
ans += e.w;
cnt++;
}
单调栈
cpp
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
if (!st.empty()) leftLess[i] = st.top();
st.push(i);
}
单调队列
cpp
deque<int> q;
for (int i = 0; i < n; i++) {
while (!q.empty() && q.front() <= i - k) q.pop_front();
while (!q.empty() && a[q.back()] <= a[i]) q.pop_back();
q.push_back(i);
if (i >= k - 1) ans.push_back(a[q.front()]);
}
树状数组
cpp
vector<long long> tr(n + 1, 0);
int lowbit(int x) {
return x & -x;
}
void add(int x, long long v) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += v;
}
long long sum(int x) {
long long res = 0;
for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];
return res;
}
线段树框架
cpp
struct Node {
int l, r;
long long sum;
} tr[N << 2];
void pushup(int u) {
tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void build(int u, int l, int r) {
tr[u] = {l, r, 0};
if (l == r) return;
int mid = (l + r) >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
}
提醒:
- Dijkstra 只适用于非负边。
- 树状数组通常配 1 下标更稳。
八、字符串模板
KMP - ne 数组
cpp
vector<int> ne(m, 0);
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && p[i] != p[j]) j = ne[j - 1];
if (p[i] == p[j]) j++;
ne[i] = j;
}
KMP - 匹配
cpp
for (int i = 0, j = 0; i < n; i++) {
while (j > 0 && s[i] != p[j]) j = ne[j - 1];
if (s[i] == p[j]) j++;
if (j == m) {
// 匹配成功
j = ne[j - 1];
}
}
Trie
cpp
const int N = 100000 + 5;
int trie[N][26], cnt[N], idx;
void insert(const string& s) {
int p = 0;
for (char ch : s) {
int u = ch - 'a';
if (!trie[p][u]) trie[p][u] = ++idx;
p = trie[p][u];
}
cnt[p]++;
}
字符串哈希
cpp
using ull = unsigned long long;
const ull P = 131;
vector<ull> h(n + 1, 0), p(n + 1, 1);
for (int i = 1; i <= n; i++) {
h[i] = h[i - 1] * P + s[i];
p[i] = p[i - 1] * P;
}
中心扩展回文
cpp
int ans = 1;
for (int c = 0; c < n; c++) {
for (int l = c, r = c; l >= 0 && r < n && s[l] == s[r]; l--, r++) {
ans = max(ans, r - l + 1);
}
for (int l = c, r = c + 1; l >= 0 && r < n && s[l] == s[r]; l--, r++) {
ans = max(ans, r - l + 1);
}
}
Manacher 框架
cpp
string build(const string& s) {
string t = "^";
for (char c : s) {
t += "#";
t += c;
}
t += "#$";
return t;
}
提醒:
- KMP 更适合模式匹配。
- 哈希更适合子串比较。
- Trie 更适合字符串集合管理。
九、数论与组合数模板
gcd / lcm
cpp
long long g = std::gcd(a, b);
long long l = a / g * b;
判素数
cpp
bool isPrime(long long x) {
if (x < 2) return false;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) return false;
}
return true;
}
线性筛
cpp
vector<int> prime;
vector<bool> vis(n + 1, false);
for (int i = 2; i <= n; i++) {
if (!vis[i]) prime.push_back(i);
for (int p : prime) {
if (1LL * i * p > n) break;
vis[i * p] = true;
if (i % p == 0) break;
}
}
质因数分解
cpp
vector<pair<long long, int>> factorize(long long x) {
vector<pair<long long, int>> res;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
int cnt = 0;
while (x % i == 0) {
x /= i;
cnt++;
}
res.push_back({i, cnt});
}
}
if (x > 1) res.push_back({x, 1});
return res;
}
快速幂
cpp
long long qmi(long long a, long long b, long long mod) {
long long res = 1 % mod;
while (b) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}
模逆元
cpp
long long inv(long long a, long long p) {
return qmi(a, p - 2, p);
}
欧拉函数
cpp
long long phi(long long x) {
long long ans = x;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
ans = ans / i * (i - 1);
while (x % i == 0) x /= i;
}
}
if (x > 1) ans = ans / x * (x - 1);
return ans;
}
裴蜀定理解是否可行
cpp
bool hasSolution(long long a, long long b, long long c) {
return c % std::gcd(a, b) == 0;
}
组合数 - Pascal
cpp
for (int i = 0; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
}
}
组合数 - 阶乘 + 逆元
cpp
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % mod;
ifac[n] = qmi(fac[n], mod - 2, mod);
for (int i = n; i >= 1; i--) ifac[i - 1] = ifac[i] * i % mod;
auto C = [&](int n, int m) -> long long {
if (m < 0 || m > n) return 0;
return fac[n] * ifac[m] % mod * ifac[n - m] % mod;
};
提醒:
- 费马小定理常要求模数是素数。
lcm先除后乘。
十、复杂度速查
| 复杂度 | 体感定位 | 常见场景 |
|---|---|---|
O(logn) |
很稳 | 二分、快速幂、树状数组单次操作 |
O(n) |
高频主力 | 扫描、前缀和、双指针、单调结构 |
O(nlogn) |
很常见 | 排序、堆、 Dijkstra |
O(n^2) |
中等规模可接受 | LIS、二维 DP、小图处理 |
O(n^3) |
只适合较小规模 | 区间 DP、小矩阵 |
O(2^n) |
只适合很小规模 | 回溯、状态压缩 |
十一、常见公式速查
区间和
text
s[r] - s[l - 1]
二维子矩形和
text
sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1]
gcd / lcm
text
gcd(a, b) = gcd(b, a % b)
lcm(a, b) = a / gcd(a, b) * b
约数个数
text
如果 n = p1^a1 * p2^a2 * ... * pk^ak
那么约数个数是 (a1 + 1)(a2 + 1)...(ak + 1)
组合数递推
text
C(n, k) = C(n - 1, k) + C(n - 1, k - 1)
十二、看到题面时的第一联想
| 题面关键词 | 第一联想 |
|---|---|
| 多次区间和 | 前缀和 |
| 多次区间改动 | 差分 |
| 最短步数 | BFS |
| 枚举全部方案 | 回溯 |
| 有依赖顺序 | 拓扑排序 |
| 非负最短路 | Dijkstra |
| 子串匹配 | KMP |
| 字符串集合 | Trie |
| 公约数 | gcd |
| 模幂 | 快速幂 |
| 选物品 | 背包 |
| 最长上升 | LIS |
十三、最后 20 分钟检查清单
先看已经写完的题
- 数组大小够不够。
- 下标有没有越界。
int是否该换long long。- 初始化是否完整。
- 多组数据是否清空。
sort区间是否正确。- 二分边界是否能停下来。
- BFS / DFS 是否重复访问。
- 模运算是否每步取模。
- 答案输出是否是题目要的那个量。
不要做的事
- 不要临时换模板风格。
- 不要在最后 10 分钟开一题完全陌生的大题。
- 不要因为一题没做出来就心态崩。
十四、手敲顺序建议
如果你准备默写模板,建议按这个顺序练:
- 比赛基础模板
- 排序 +
lower_bound - 前缀和 + 差分
- 双指针
- DFS / BFS
- 0-1 背包
- 并查集 + Dijkstra
- KMP + Trie
- 快速幂 + 组合数
十五、比赛前的最短复习路线
如果你只剩很少时间,就按下面顺序:
- 看比赛基础模板。
- 看前缀和 / 差分 / 双指针。
- 看 DFS / BFS。
- 看背包和 LIS。
- 看并查集 / Dijkstra。
- 看 KMP / Trie。
- 看快速幂 / gcd / 组合数。
- 看最后 20 分钟检查清单。
十六、模板默写任务表
| 任务编号 | 模板名称 | 目标时间 | 合格标准 |
|---|---|---|---|
| 1 | 比赛基础模板 | 1 分钟 | 不看资料写完整 |
| 2 | 排序 + 二分函数 | 2 分钟 | sort、lower_bound、upper_bound 正确 |
| 3 | 基础二分 | 2 分钟 | 不死循环 |
| 4 | 二分答案 | 3 分钟 | check 思路清楚 |
| 5 | 一维前缀和 | 2 分钟 | 区间和公式不写错 |
| 6 | 二维前缀和 | 3 分钟 | 容斥公式正确 |
| 7 | 一维差分 | 2 分钟 | r + 1 不越界 |
| 8 | 对撞指针 | 2 分钟 | 指针移动方向正确 |
| 9 | 滑动窗口 | 3 分钟 | 更新答案时机正确 |
| 10 | 位运算基础 | 2 分钟 | 1LL << k 熟练 |
| 11 | 排列回溯 | 4 分钟 | used 和恢复状态完整 |
| 12 | 网格 BFS | 4 分钟 | 方向数组和 dist 正确 |
| 13 | 0-1 背包 | 3 分钟 | 逆序枚举容量 |
| 14 | 完全背包 | 3 分钟 | 正序枚举容量 |
| 15 | LIS | 4 分钟 | dp[i] 定义清晰 |
| 16 | 并查集 | 3 分钟 | find + merge 正确 |
| 17 | Dijkstra | 5 分钟 | 堆优化 + 判重完整 |
| 18 | KMP | 6 分钟 | ne 和匹配都能写 |
| 19 | Trie | 4 分钟 | 插入和查询不越界 |
| 20 | 快速幂 + 组合数 | 5 分钟 | 模运算和逆元正确 |
十七、口袋题型速配表
| 题目现象 | 第一联想 | 第二联想 | 最容易错的点 |
|---|---|---|---|
| 多次区间和 | 前缀和 | 二维前缀和 | 下标和类型 |
| 多次区间修改 | 差分 | 二维差分 | r + 1 越界 |
| 最短步数 | BFS | 多源 BFS | 访问标记时机 |
| 连通块数量 | DFS / BFS | 并查集 | 是否重复访问 |
| 最长连续合法区间 | 滑动窗口 | 双指针 | 收缩条件 |
| 两数和 / 配对 | 排序 + 对撞指针 | 二分 | 忘记排序 |
| 模式串匹配 | KMP | 哈希 | 题型误判 |
| 字符串集合 | Trie | 哈希表 | 节点数不足 |
| 子串比较 | 哈希 | KMP 不太适合 | 前缀哈希下标 |
| 选物品 | 背包 | 分组背包 | 循环方向 |
| 最优路径 | 网格 DP | BFS 看是否无权 | 初始化 |
| 先后依赖 | 拓扑排序 | DAG DP | 入度维护 |
| 非负最短路 | Dijkstra | BFS 仅无权可用 | 有负边仍硬写 |
| 动态连通性 | 并查集 | 图搜索 | 初始化 |
| 连接所有点最小代价 | Kruskal | Prim 可选 | 是否连通 |
| 最近更大 / 更小 | 单调栈 | 栈存下标 | 比较符号 |
| 固定窗口最值 | 单调队列 | 堆 | 过期判断 |
| 值域大但值少 | 离散化 | sort + unique |
漏收集点 |
| 与公约数有关 | gcd | 质因数分解 | lcm 溢出 |
| 与模幂有关 | 快速幂 | 逆元 | 忘 % mod |
| 组合数很多次查询 | 阶乘 + 逆元 | Pascal | 模数条件 |
| 需要判素数 | 试除 | 筛法 | 把 1 当素数 |
| 需要批量素数 | 筛法 | 线性筛 | 空间估计 |
| 小规模选或不选 | 回溯 | 状态压缩 | 复杂度爆炸 |
| 需要所有排列 | 回溯 | next_permutation |
去重与恢复 |
| 连续区间异或 | 前缀异或 | 位运算 | 混淆加法前缀 |
| 区间合并最值 | 区间 DP | 贪心未必可行 | 枚举顺序 |
| 字符串回文 | 中心扩展 | Manacher | 奇偶都要写 |
| 图是 DAG 且求最优 | 拓扑 + DP | 记忆化搜索 | 顺序问题 |
| 查询前缀统计 | 树状数组 | 前缀和 | 是否动态修改 |
十八、极限样例清单
在提交前,可以手造这些极限样例:
n = 1- 所有值相同
- 严格递增
- 严格递减
- 全是
0 - 全是最大值
- 只有起点没有终点
- 起点终点重合
- 图不连通
- 字符串长度为
1 - 模数为
1 - 查询区间是整个数组
- 查询区间只有一个点
- 二分答案刚好卡边界
- 背包容量刚好等于某物品体积
- 所有物品都装不下
- BFS 起点周围全是墙
- DFS 只有一个连通块
- Trie 只插入一个单词
- KMP 模式串和主串完全相同
- KMP 模式串长度为
1 - 组合数
m = 0 - 组合数
m = n gcd(a, 0)- 质数判定
x = 1 - 线段树只有一个点
- 树状数组只修改最后一个位置
- 单调队列窗口长度为
1 - 滑动窗口永远不合法
- 滑动窗口从头到尾都合法
十九、赛前自测 60 问
基础与实现
-
- 你能不看资料写出比赛基础模板吗?
-
- 你知道什么时候该用
long long吗?
- 你知道什么时候该用
-
- 你会在数组题里统一下标风格吗?
-
- 你能区分
sort(a + 1, a + n + 1)和sort(a, a + n)吗?
- 你能区分
-
- 你知道
lower_bound和upper_bound的区别吗?
- 你知道
-
- 你知道
unique后还要erase吗?
- 你知道
-
- 你知道
priority_queue默认是大根堆吗?
- 你知道
-
- 你知道
queue没有clear()吗?
- 你知道
-
- 你知道
map[key]会自动插入吗?
- 你知道
-
- 你知道
memset适合初始化哪些值吗?
- 你知道
高频算法
-
- 你能写一维前缀和吗?
-
- 你能写二维前缀和吗?
-
- 你能写一维差分吗?
-
- 你知道差分和前缀和的关系吗?
-
- 你能写对撞指针吗?
-
- 你能写滑动窗口吗?
-
- 你知道什么时候适合双指针吗?
-
- 你会
1LL << k吗?
- 你会
-
- 你会写
lowbit吗?
- 你会写
-
- 你能枚举一个集合的所有子集吗?
搜索
-
- 你能清楚定义 DFS 函数参数的意义吗?
-
- 你能写排列回溯吗?
-
- 你能写组合回溯吗?
-
- 你会做选择后恢复状态吗?
-
- 你知道什么时候应该剪枝吗?
-
- 你能写网格 DFS 吗?
-
- 你能写网格 BFS 吗?
-
- 你知道为什么最短步数更常用 BFS 吗?
-
- 你知道多源 BFS 怎么初始化吗?
-
- 你能把暴力搜索改成记忆化搜索吗?
动态规划
-
- 你知道状态定义要先写中文吗?
-
- 你能写一维线性 DP 吗?
-
- 你能写 LIS 吗?
-
- 你能写 LCS 吗?
-
- 你知道 0-1 背包为什么逆序吗?
-
- 你知道完全背包为什么正序吗?
-
- 你能写网格 DP 吗?
-
- 你知道区间 DP 的枚举顺序吗?
-
- 你知道最终答案在哪个状态里吗?
-
- 你有因为初始化写错导致 DP 挂掉过吗?
图论与字符串
-
- 你能写并查集吗?
-
- 你能写拓扑排序吗?
-
- 你能写 Dijkstra 吗?
-
- 你知道 Dijkstra 不能直接处理负边吗?
-
- 你能写 Kruskal 吗?
-
- 你会单调栈吗?
-
- 你会单调队列吗?
-
- 你会树状数组吗?
-
- 你能写 KMP 的
ne数组吗?
- 你能写 KMP 的
-
- 你能写 Trie 的插入和查询吗?
数论与比赛
-
- 你能写判素数吗?
-
- 你能写筛法吗?
-
- 你能写质因数分解吗?
-
- 你能写快速幂吗?
-
- 你知道费马小定理常见适用条件吗?
-
- 你能写组合数的 Pascal 递推吗?
-
- 你能写阶乘 + 逆元求组合数吗?
-
- 你有自己的最后 20 分钟检查顺序吗?
-
- 你知道比赛里该先做哪些题吗?
-
- 你知道自己最容易错的 5 个点是什么吗?
二十、空白默写框架
比赛基础模板空框
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = ____________;
int main() {
ios::sync_with_stdio(__________);
cin.tie(__________);
return 0;
}
二分空框
cpp
int l = ____________, r = ____________;
while (l <= r) {
int mid = ________________________;
if (________________) {
________________________;
} else {
________________________;
}
}
前缀和空框
cpp
vector<long long> s(n + 1, 0);
for (int i = 1; i <= n; i++) {
s[i] = ________________________;
}
差分空框
cpp
diff[l] += ________;
diff[r + 1] ________ ________;
for (int i = 1; i <= n; i++) {
diff[i] += ________________________;
}
回溯空框
cpp
void dfs(int step) {
if (________________) {
return;
}
for (int i = ____________; i ____________; i++) {
if (________________) continue;
________________________;
dfs(________________);
________________________;
}
}
BFS 空框
cpp
queue<pair<int, int>> q;
q.push({________, ________});
while (!q.empty()) {
auto [x, y] = q.front();
q.pop();
for (int i = 0; i < 4; i++) {
int nx = ________________________;
int ny = ________________________;
if (________________) continue;
________________________;
q.push({nx, ny});
}
}
0-1 背包空框
cpp
for (int i = 1; i <= n; i++) {
for (int j = ________; j >= ________; j--) {
dp[j] = ________________________;
}
}
并查集空框
cpp
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = ________________________;
}
Dijkstra 空框
cpp
priority_queue<PII, vector<PII>, greater<PII>> pq;
dist[s] = ________;
pq.push({________, s});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (________________) continue;
________________________;
}
KMP 空框
cpp
for (int i = 1, j = 0; i < m; i++) {
while (j > 0 && __________________) j = __________________;
if (________________) j++;
ne[i] = ________;
}
快速幂空框
cpp
long long qmi(long long a, long long b, long long mod) {
long long res = ________;
while (b) {
if (________) res = ________________________;
a = ________________________;
b >>= 1;
}
return res;
}
二十一、专题填空默写版(上)
二维前缀和空框
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
sum[i][j] = ________________________
+ ________________________
- ________________________
+ ________________________;
}
}
二维差分空框
cpp
auto add = [&](int x1, int y1, int x2, int y2, long long c) {
d[x1][y1] += ________;
d[x2 + 1][y1] ________ c;
d[x1][y2 + 1] ________ c;
d[x2 + 1][y2 + 1] ________ c;
};
lower_bound 统计次数空框
cpp
int L = lower_bound(a + 1, a + n + 1, x) - a;
int R = upper_bound(a + 1, a + n + 1, x) - a;
int cnt = ________________________;
滑动窗口空框
cpp
int l = 0;
for (int r = 0; r < n; r++) {
// 加入 a[r]
while (________________) {
// 移除 a[l]
________________________;
l++;
}
// 更新答案
}
LIS 空框
cpp
vector<int> dp(n, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < i; j++) {
if (________________) {
dp[i] = ________________________;
}
}
}
LCS 空框
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = max(________________, ________________);
if (________________) {
dp[i][j] = max(dp[i][j], ________________________);
}
}
}
网格 DP 空框
cpp
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
dp[i][j] = ________________________;
}
}
区间 DP 空框
cpp
for (int len = 2; len <= n; len++) {
for (int l = 1; l + len - 1 <= n; l++) {
int r = ________________________;
dp[l][r] = ________________________;
for (int k = l; k < r; k++) {
dp[l][r] = ________________________;
}
}
}
拓扑排序空框
cpp
for (int i = 1; i <= n; i++) {
if (deg[i] == 0) q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v : g[u]) {
if (________________) q.push(v);
}
}
Kruskal 空框
cpp
sort(edges.begin(), edges.end());
for (auto &e : edges) {
int fu = find(e.u), fv = find(e.v);
if (fu == fv) continue;
fa[fu] = fv;
ans += ________;
cnt++;
}
二十二、专题填空默写版(下)
单调栈空框
cpp
stack<int> st;
for (int i = 1; i <= n; i++) {
while (!st.empty() && __________________) st.pop();
if (!st.empty()) leftLess[i] = ________________________;
st.push(________________);
}
单调队列空框
cpp
deque<int> q;
for (int i = 0; i < n; i++) {
while (!q.empty() && __________________) q.pop_front();
while (!q.empty() && __________________) q.pop_back();
q.push_back(i);
}
树状数组空框
cpp
int lowbit(int x) {
return ________________________;
}
void add(int x, long long v) {
for (int i = x; i <= n; i += ________________________) {
tr[i] += v;
}
}
线段树建树空框
cpp
void build(int u, int l, int r) {
tr[u] = {l, r, 0};
if (________________) return;
int mid = ________________________;
build(u << 1, l, mid);
build(u << 1 | 1, ________________________, r);
}
Trie 查询空框
cpp
int query(const string& s) {
int p = 0;
for (char ch : s) {
int u = ________________________;
if (!trie[p][u]) return 0;
p = ________________________;
}
return ________________________;
}
哈希取子串空框
cpp
auto getHash = [&](int l, int r) {
return ________________________;
};
欧拉函数空框
cpp
long long phi(long long x) {
long long ans = x;
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
ans = ________________________;
while (x % i == 0) x /= i;
}
}
if (x > 1) ans = ________________________;
return ans;
}
Pascal 组合数空框
cpp
for (int i = 0; i <= n; i++) {
C[i][0] = C[i][i] = 1;
for (int j = 1; j < i; j++) {
C[i][j] = ________________________;
}
}
质因数分解空框
cpp
for (long long i = 2; i <= x / i; i++) {
if (x % i == 0) {
int cnt = 0;
while (x % i == 0) {
x /= i;
cnt++;
}
________________________;
}
}
if (x > 1) ________________________;
阶乘 + 逆元空框
cpp
fac[0] = 1;
for (int i = 1; i <= n; i++) fac[i] = ________________________;
ifac[n] = ________________________;
for (int i = n; i >= 1; i--) {
ifac[i - 1] = ________________________;
}
二十三、80 条高频错点
-
sort右端点少写一位。
-
lower_bound用在无序区间。
-
unique后忘记erase。
-
1 << 40没写成1LL << 40。
-
- 前缀和数组开成了
int。
- 前缀和数组开成了
-
- 差分的
r + 1越界。
- 差分的
-
- 双指针数组没排序。
-
- 滑动窗口条件写反。
-
- DFS 没写出口。
-
- 回溯忘记恢复状态。
-
used数组多组数据没清空。
-
- BFS 没在合适时机标记访问。
-
- BFS 起点没初始化距离。
-
- DFS / BFS 坐标体系不统一。
-
- 记忆化搜索的"未计算标记"没统一。
-
- DP 状态定义自己都说不清。
-
- DP 初始值错误。
-
- 0-1 背包循环方向写成正序。
-
- 完全背包循环方向写成逆序。
-
- LIS 把子序列当子数组。
-
- 区间 DP 的长度枚举顺序错。
-
- 网格 DP 忘记处理障碍。
-
- Dijkstra 用在负边图上。
-
- 堆里旧状态没过滤。
-
- 并查集忘记初始化。
-
- Kruskal 做完没判连通。
-
- 拓扑排序没检查是否存在环。
-
- 单调栈存值还是存下标没分清。
-
- 单调队列窗口过期条件错。
-
- 树状数组从
0开始更新。
- 树状数组从
-
- 线段树左右孩子区间写错。
-
- 线段树空间没开
4n。
- 线段树空间没开
-
- KMP 的
j回退写错。
- KMP 的
-
- KMP 成功匹配后没继续回退。
-
- Trie 节点数估小。
-
- 字符集不是
26还写死26。
- 字符集不是
-
- 哈希数组下标不统一。
-
- 回文题漏写偶数中心。
-
getline被残留换行影响。
-
- 把简单模拟题写成重型字符串算法题。
-
gcd/lcm边界没考虑0。
-
lcm先乘后除溢出。
-
- 把
1当成素数。
- 把
-
- 试除法循环上界写错。
-
- 筛法数组空间没估够。
-
- 质因数分解漏掉最后的大素因子。
-
- 快速幂忘记每步
% mod。
- 快速幂忘记每步
-
- 逆元在不适用的模数上乱用。
-
- 欧拉函数公式记混。
-
- 裴蜀定理只会背不会判条件。
-
- Pascal 递推边界没初始化。
-
- 阶乘逆元预处理终点错。
-
- 模减法忘记防负数。
-
- 多组数据没清空答案变量。
-
vector越界访问。
-
- 空栈访问
top()。
- 空栈访问
-
- 空队列访问
front()。
- 空队列访问
-
map[key]自动插入导致逻辑变化。
-
priority_queue方向反了。
-
- 自定义比较器关键字顺序写反。
-
- 图是有向还是无向没确认。
-
- 边权和距离还用
int。
- 边权和距离还用
-
- 样例过了就直接交,没有手造边界。
-
- 最后答案位置看错。
-
- 复杂度只看单次,没看总次数。
-
- 多次查询每次都重新排序。
-
- 递归深度太深没防爆栈。
-
- 需要
long long的地方偷懒用int。
- 需要
-
- 数组大小卡得太死。
-
- 用
memset给long long设大值。
- 用
-
- 二分边界不收缩导致死循环。
-
- 二分答案没有单调性还强上。
-
check函数写对了吗没有单独验证。
-
- 离散化漏收集查询值。
-
- 题目要的是方案数却写成最优值。
-
- 题目要的是最小值却用
max。
- 题目要的是最小值却用
-
- 列表 / 路径输出顺序没确认。
-
- 真题模拟时一题卡太久不跳。
-
- 最后 20 分钟没检查已做题。
-
- 没有自己的错题分类体系。
二十四、50 组常见混淆对照
| 容易混淆的点 | 正确区分方式 |
|---|---|
| 子数组 vs 子序列 | 子数组连续,子序列不一定连续 |
| 0-1 背包 vs 完全背包 | 是否允许重复选同一物品 |
| DFS vs BFS | 全部枚举常 DFS,最少步数常 BFS |
| KMP vs 哈希 | 模式匹配 vs 子串比较 |
Trie vs map |
前缀树管理结构 vs 普通键值映射 |
| 前缀和 vs 差分 | 一个擅长查区间,一个擅长改区间 |
| 对撞指针 vs 滑动窗口 | 有序配对 vs 连续区间 |
| 并查集 vs DFS 连通块 | 动态合并关系 vs 静态遍历 |
| Dijkstra vs BFS | 非负带权 vs 无权 |
| Kruskal vs 最短路 | 连接所有点最小总代价 vs 单源最短路 |
| 单调栈 vs 单调队列 | 最近更值 vs 窗口最值 |
| 树状数组 vs 线段树 | 轻量前缀结构 vs 更通用区间结构 |
| 判素数 vs 筛法 | 单次判断 vs 批量预处理 |
| Pascal vs 阶乘逆元 | 小范围递推 vs 多次组合数查询 |
| 记忆化搜索 vs 递推 DP | 搜着记 vs 直接推 |
二十五、50 组极限样例提醒
-
- 数组长度为
1。
- 数组长度为
-
- 字符串长度为
1。
- 字符串长度为
-
- 图只有一个点。
-
- 图没有边。
-
- 图不连通。
-
- 所有数组元素相同。
-
- 所有数组元素严格递增。
-
- 所有数组元素严格递减。
-
- 所有边权相同。
-
- 所有边权都很大。
-
- 起点就是终点。
-
- BFS 起点四周全是障碍。
-
- 滑动窗口长度为
1。
- 滑动窗口长度为
-
- 滑动窗口长度等于
n。
- 滑动窗口长度等于
-
- 二分答案就在左边界。
-
- 二分答案就在右边界。
-
mod = 1。
-
a = 0或b = 0。
-
gcd(a, b) = 1。
-
gcd(a, b) = a。
-
- 组合数
m = 0。
- 组合数
-
- 组合数
m = n。
- 组合数
-
- 组合数
m > n。
- 组合数
-
- Trie 中只有一个单词。
-
- Trie 查询不存在的单词。
-
- KMP 主串和模式串完全相同。
-
- KMP 模式串只有一个字符。
-
- 哈希比较同一段子串。
-
- 回文串全相同字符。
-
- 回文串完全没有长度大于
1的回文。
- 回文串完全没有长度大于
-
- 并查集中重复合并同一对点。
-
- Kruskal 边数不足。
-
- 拓扑图有环。
-
- 树状数组修改最后一个点。
-
- 树状数组查询前缀
0。
- 树状数组查询前缀
-
- 线段树区间只剩一个点。
-
- 前缀和查询整个区间。
-
- 差分修改只改一个点。
-
- 双指针找不到解。
-
- LIS 全是相等元素。
-
- LCS 两串完全不同。
-
- 背包容量比所有物品都小。
-
- 背包容量恰好装满。
-
- 所有价值为
0。
- 所有价值为
-
- DP 初始状态就是最终答案。
-
- 搜索只有一条路径。
-
- 搜索分支极多但早剪枝。
-
- 多组数据第一组和第二组规模差很大。
-
- 输入有负数。
-
- 输出要求取模但答案可能先变负。
二十六、60 条赛场口令
-
- 先看范围,再想算法。
-
- 先做稳题,不先碰最难题。
-
- 样例过了不等于题过了。
-
- 一题卡太久要敢跳。
-
- 写前先确认下标体系。
-
- 写前先确认类型范围。
-
- 二分先想单调性。
-
- 搜索先想状态和出口。
-
- DP 先写中文状态定义。
-
- 图题先确认有向还是无向。
-
- 最短步数优先想 BFS。
-
- 非负最短路优先想 Dijkstra。
-
- 区间和优先想前缀和。
-
- 区间修改优先想差分。
-
- 连续区间优先想双指针。
-
- 多次组合数查询优先想预处理。
-
- 字符串匹配优先判断要不要 KMP。
-
- 字符串集合优先想 Trie。
-
- 模幂题先把快速幂写出来。
-
- 取模减法先防负数。
-
lcm先除后乘。
-
- 复杂度要看总次数。
-
- 排序后很多题会简单很多。
-
unique后别忘erase。
-
priority_queue默认是大根堆。
-
map[key]会自动插入。
-
queue没有clear()。
-
memset不是万能初始化。
-
1LL << k比1 << k稳。
-
- 滑动窗口要明确何时收缩。
-
- 回溯固定写"做选择 -> 递归 -> 撤销"。
-
- BFS 通常入队就标记。
-
- 并查集每组数据都要重置。
-
- 树状数组最好用 1 下标。
-
- 线段树空间先开
4n。
- 线段树空间先开
-
- Dijkstra 不是万能最短路。
-
- Kruskal 做完要检查是否连通。
-
- 拓扑排序能顺便判环。
-
- LIS 是子序列,不是子数组。
-
- 背包方向一错,全题都错。
-
- 0-1 背包逆序。
-
- 完全背包正序。
-
- 组合回溯要靠
start去重。
- 组合回溯要靠
-
- 排列回溯要靠
used去重。
- 排列回溯要靠
-
- 看到"最小的最大值"想二分答案。
-
- 看到"互质个数"想欧拉函数。
-
- 看到"ax + by = c"想裴蜀定理。
-
- 看到"前 i 个"常想线性 DP。
-
- 看到"每组选一个"想分组背包。
-
- 看到"依赖顺序"想拓扑排序。
-
- 看到"当前窗口最值"想单调队列。
-
- 看到"最近更小 / 更大"想单调栈。
-
- 看到"动态连通"想并查集。
-
- 看到"值域大但值少"想离散化。
-
- 最后 20 分钟优先查已做题。
-
- 不要临场发明新模板。
-
- 不要轻易换自己熟悉的风格。
-
- 不要把会做的题写挂。
-
- 题没做出也别乱心态。
-
- 赛后一定要复盘错误类型。
二十七、50 条模板提醒
-
- 比赛模板越短越好。
-
- 常量写在最前面更稳。
-
ios::sync_with_stdio(false);几乎默认要写。
-
- 排序和二分最好配套记忆。
-
- 前缀和默认开
long long。
- 前缀和默认开
-
- 差分数组多开一位。
-
- 二维数组边界要比一维更小心。
-
- 双指针数组有序时更常见。
-
- 快慢指针不只用于链表。
-
- 子集枚举只适合小
n。
- 子集枚举只适合小
-
- DFS 参数意义一定要清楚。
-
- BFS 队列里的元素结构要先设计好。
-
- 记忆化搜索的"未访问标记"要统一。
-
- 背包模板手感比记忆更重要。
-
- LIS 的
dp[i]是"以 i 结尾"很常见。
- LIS 的
-
- 网格 DP 起点初始化必须单独看。
-
- 区间 DP 先枚举长度。
-
- 并查集
find建议直接路径压缩。
- 并查集
-
- Dijkstra 需要小根堆。
-
- 边权和距离尽量开
long long。
- 边权和距离尽量开
-
- Kruskal 的边要先排序。
-
- 单调栈通常存下标更灵活。
-
- 单调队列也常存下标。
-
- 树状数组最核心就是
lowbit。
- 树状数组最核心就是
-
- 线段树先会
build和pushup。
- 线段树先会
-
- KMP 先背
ne,再背匹配。
- KMP 先背
-
- Trie 的节点总数要按总字符量估计。
-
- 哈希最好统一 1 下标。
-
- 回文中心扩展要写奇偶两种。
-
- 快速幂每一步都要
% mod。
- 快速幂每一步都要
-
- 判素数时
x < 2直接假。
- 判素数时
-
- 质因数分解别漏最后的大素因子。
-
- 组合数
m > n直接是 0。
- 组合数
-
- 逆元模板要先确认模数条件。
-
gcd(a, 0)这种边界别忘。
-
lcm先除后乘不是形式主义。
-
- 离散化后编号从 0 还是 1 要统一。
-
- 多组数据题清空比重写更重要。
-
- 容器题要先想是否需要顺序。
-
unordered_map不一定总比map好。
-
- 样例少时更要自己造边界。
-
- 模板写得太花哨反而容易出错。
-
- 比赛中模板应尽量用自己练过的版本。
-
- 不熟的模板宁可少写,也别半懂硬上。
-
- 抄模板前先知道它解决什么问题。
-
- 题型不对,模板越标准也没用。
-
- 模板只是骨架,变量意义要看题改。
-
- 赛前至少整体默写一次附录。
-
- 错题要反推到模板哪一行不熟。
-
- 真正掌握模板的标准是不看资料能写出 80%。
二十八、20 组题型信号速读
-
- "区间和很多" -> 前缀和。
-
- "区间修改很多" -> 差分。
-
- "最短步数" -> BFS。
-
- "所有方案" -> 回溯。
-
- "非负最短路" -> Dijkstra。
-
- "动态连通" -> 并查集。
-
- "有依赖关系" -> 拓扑排序。
-
- "连接所有点最小代价" -> Kruskal。
-
- "连续区间最值" -> 滑动窗口。
-
- "两端夹逼" -> 对撞指针。
-
- "模式匹配" -> KMP。
-
- "词典管理" -> Trie。
-
- "子串比较" -> 哈希。
-
- "最长回文" -> 中心扩展 / Manacher。
-
- "选物品" -> 背包。
-
- "前 i 个最优" -> 线性 DP。
-
- "互质个数" -> 欧拉函数。
-
- "模幂" -> 快速幂。
-
- "约数个数" -> 质因数分解。
-
- "组合数多次查询" -> 阶乘 + 逆元。
二十九、补充默写框架
组合回溯空框
cpp
void dfs(int start) {
if ((int)path.size() == k) {
return;
}
for (int i = ________; i <= n; i++) {
________________________;
dfs(________________);
________________________;
}
}
网格 DFS 空框
cpp
void dfs(int x, int y) {
vis[x][y] = true;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (________________) continue;
if (________________) continue;
dfs(nx, ny);
}
}
多源 BFS 空框
cpp
for (auto [x, y] : sources) {
dist[x][y] = ________;
q.push({x, y});
}
完全背包空框
cpp
for (int i = 1; i <= n; i++) {
for (int j = ________; j <= V; j++) {
dp[j] = ________________________;
}
}
分组背包空框
cpp
for (int i = 1; i <= g; i++) {
for (int j = V; j >= 0; j--) {
for (auto [vol, val] : group[i]) {
if (j >= vol) {
dp[j] = ________________________;
}
}
}
}
并查集合并空框
cpp
void merge(int a, int b) {
a = ________________________;
b = ________________________;
if (a != b) fa[a] = ________;
}
拓扑排序判环空框
cpp
while (!q.empty()) {
int u = q.front();
q.pop();
order.push_back(u);
for (int v : g[u]) {
if (--deg[v] == 0) q.push(v);
}
}
bool hasCycle = ((int)order.size() ________ n);
单调栈求左边最近更小空框
cpp
while (!st.empty() && a[st.top()] >= a[i]) st.pop();
if (!st.empty()) leftLess[i] = ________;
st.push(________);
树状数组求前缀和空框
cpp
long long sum(int x) {
long long res = 0;
for (int i = x; i > 0; i -= ________________________) {
res += ________________________;
}
return res;
}
组合数查询空框
cpp
auto C = [&](int n, int m) -> long long {
if (m < 0 || m > n) return ________;
return fac[n] * ifac[m] % mod * ________________________ % mod;
};
三十、40 条查错问题
-
- 数组是不是开小了?
-
- 下标是不是混用了 0 和 1?
-
sort区间真的写对了吗?
-
- 二分区间真的有序吗?
-
- 二分更新方向会不会死循环?
-
check函数真的单调吗?
-
long long用够了吗?
-
- 中间乘法是不是先溢出了?
-
- 前缀和公式有没有写反?
-
- 差分恢复有没有漏原数组?
-
- 滑动窗口收缩条件正确吗?
-
- DFS 出口写了吗?
-
- 回溯恢复状态了吗?
-
- BFS 访问标记时机对吗?
-
- 多组数据清空了吗?
-
- DP 初始值合理吗?
-
- DP 答案位置找对了吗?
-
- 背包循环方向对吗?
-
- 图边都加全了吗?
-
- 有向图误加反向边了吗?
-
- Dijkstra 图里有负边吗?
-
- Kruskal 是否连通了?
-
- 树状数组是不是从 1 下标开始?
-
- 线段树孩子区间划分对吗?
-
- KMP 的
j回退写对了吗?
- KMP 的
-
- Trie 字符集对应对了吗?
-
- 哈希下标统一了吗?
-
- 质因数分解最后剩余值处理了吗?
-
- 快速幂每步都
% mod了吗?
- 快速幂每步都
-
- 逆元适用条件满足吗?
-
- 组合数
m > n处理了吗?
- 组合数
-
- 模减法防负数了吗?
-
- 输出的是题目真正要的答案吗?
-
- 边界样例自己造了吗?
-
- 极小值样例试了吗?
-
- 极大值样例试了吗?
-
- 所有元素相同试了吗?
-
- 起点终点重合试了吗?
-
- 只剩一个元素的情况试了吗?
-
- 提交前有没有从头顺一遍逻辑?
三十一、50 条复盘口令
-
- 只看答案不算复盘。
-
- 只看题解不算补题。
-
- 会做但写挂最值得复盘。
-
- 没想到题型要回章节补模型。
-
- 想到了模板却写不顺,要练默写。
-
- TLE 先看复杂度,不先怪编译器。
-
- WA 先看边界,不先怀疑平台。
-
- RE 先看数组和越界。
-
- 多组数据错先看初始化。
-
- 二分错先看单调和边界。
-
- 搜索错先看状态恢复。
-
- DP 错先看状态定义。
-
- 图题错先看建边方式。
-
- 字符串错先看题型判断。
-
- 数论错先看模板适用条件。
-
- 错题要写"为什么错",不是只记题号。
-
- 一类题错两次,就该单独建专题。
-
- 会做但超时,也算没拿到分。
-
- 不会做的题要归类,不要只是沮丧。
-
- 复盘后的动作必须具体。
-
- 明天补什么要写成一句话。
-
- 复盘时优先找共性错误。
-
- 模板题做错,问题往往不是题目难。
-
- 错因要写到"哪一行可能出错"。
-
- 真题复盘比单题复盘更能看出节奏问题。
-
- 模拟赛后先复盘,不先开新卷。
-
- 如果总卡在实现,说明基础不稳。
-
- 如果总卡在题型判断,说明模型不熟。
-
- 如果总卡在时间分配,说明比赛策略要练。
-
- 复盘不是否定自己,是为了下次更快。
-
- 错题二刷比新题乱刷更有价值。
-
- 记住"哪里浪费了时间"。
-
- 记住"哪题本来该拿到"。
-
- 记住"为什么没敢跳题"。
-
- 记住"为什么明知会做却没写出来"。
-
- 每周至少整理一次复盘表。
-
- 一道题至少能归到一个章节。
-
- 如果归不到章节,说明体系还不够清楚。
-
- 复盘最终要回到模板和判断力。
-
- 复盘后要有一次再做验证。
-
- 不验证的复盘容易变成感想。
-
- 能一句话说出错因,说明你真的理解了。
-
- 能避免第二次再犯,复盘才算有效。
-
- 不要把"我粗心了"当最终结论。
-
- 粗心往往背后是流程缺失。
-
- 没有检查顺序,就容易反复粗心。
-
- 模板越熟,复盘越能聚焦到真正问题。
-
- 题目做对了,也要想有没有更稳写法。
-
- 题目做快了,也要想有没有更短模板。
-
- 最终目标不是刷更多题,而是拿更稳定的分。
三十二、补充默写框架
前缀计数空框
cpp
for (int i = 1; i <= n; i++) {
cnt[i] = cnt[i - 1] + (________________);
}
前缀异或空框
cpp
for (int i = 1; i <= n; i++) {
pre[i] = ________________________;
}
对撞指针空框
cpp
int l = 0, r = n - 1;
while (l < r) {
long long sum = ________________________;
if (sum == target) break;
if (sum < target) ________;
else ________;
}
快慢指针空框
cpp
int j = 0;
for (int i = 0; i < n; i++) {
if (________________) {
a[j++] = ________________________;
}
}
子集枚举空框
cpp
for (int mask = 0; mask < (1 << n); mask++) {
// ________________________
}
记忆化搜索空框
cpp
int dfs(int x) {
if (dp[x] != -1) return ________________________;
int res = ________________________;
// 转移
return dp[x] = ________________________;
}
LIS 取答案空框
cpp
int ans = ________________________;
单调队列输出空框
cpp
if (i >= k - 1) ans.push_back(________________);
判素数空框
cpp
bool isPrime(long long x) {
if (x < 2) return false;
for (long long i = 2; i <= ________; i++) {
if (x % i == 0) return false;
}
return true;
}
逆元空框
cpp
long long inv(long long a, long long p) {
return ________________________;
}
三十三、20 条时间分配提醒
-
- 开局先扫题。
-
- 第一题不要超过你的预设时间。
-
- 稳题先拿分。
-
- 中档题要设止损时间。
-
- 卡住就记状态后跳题。
-
- 不要反复从头读同一题。
-
- 会做但实现长的题先评估值不值。
-
- 最后 30 分钟优先检查。
-
- 最后 10 分钟别开新大题。
-
- 一题 AC 后先稳住情绪。
-
- 两题都卡时先保最熟的。
-
- 不要被别人节奏影响。
-
- 不要因为一题失利放弃整场。
-
- 已拿的分比未知分更重要。
-
- 真题模拟时就要练这个节奏。
-
- 节奏也是能力的一部分。
-
- 会做但没时间写完,等于没拿到。
-
- 每场赛后都要复盘时间线。
-
- 你要知道自己哪类题最耗时。
-
- 时间分配不稳定,分数就不稳定。
三十四、15 条赛后动作提醒
-
- 当天整理错题类型。
-
- 第二天重做最可惜的题。
-
- 一周内再做一次同类题。
-
- 把新学到的模板收进附录。
-
- 删掉自己不会用的花哨写法。
-
- 对高频错误建立个人清单。
-
- 给每个章节留出"补弱题位"。
-
- 模拟赛和专题题要交替进行。
-
- 模板会写不代表比赛一定会用。
-
- 要把"想到"训练成"写出"。
-
- 要把"写出"训练成"写稳"。
-
- 要把"写稳"训练成"写快"。
-
- 每次复盘都要有下一步动作。
-
- 只做记录没有行动不算升级。
-
- 真正的进步来自长期重复。
结语
最后还是那句话:比赛中最怕的不是不会,而是本来会做却因为模板不熟、边界不稳、时间分配失衡而丢分。希望这篇文章能帮你把"会一点"变成"会得稳"。