蓝桥杯C++组算法知识点整理(考前急救)

蓝桥杯 C++ 组算法知识点整理·

写在前面

这篇文章我没有按"只适合考前半小时翻"的速查风格去写,而是按"可以长期收藏、反复回看、边学边练"的长文去整理。读者大大可以把它理解成一份围绕蓝桥杯 C++ 组的完整知识手册:前面讲基础和题型,中间讲核心算法,后面讲训练路线、真题策略和模板压缩。

目录

这篇文章适合谁看

  • 正在准备蓝桥杯 C++ 组,想系统过一遍知识点的人。
  • 平时刷过一些题,但专题之间没有串起来的人。

正文

一、C++ 基础与 STL

本章适合谁

  • 刚开始准备蓝桥杯,担心语言基础不稳的人。
  • 做题经常因为 sortvector、下标、类型写错而丢分的人。
  • 明明知道算法思路,却总在实现阶段卡壳的人。

建议前置知识

  • 会写最基本的 forif、函数。
  • 知道数组、字符串、结构体的基本概念。
  • 能看懂简单的 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 的输入输出,速度较慢。
  • 关闭同步并解绑 cincout 后,竞赛里一般够用。

模板:

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)

典型题型:

  • 区间排序、成绩排序、二维关键字排序。

易错点:

  • 多关键字顺序写反。

推荐练习:

  • 写一个"先按第一关键字升序,再按第二关键字降序"的排序题。
reverseunique

什么时候用:

  • 需要反转序列或排序后去重。

核心思路:

  • unique 只会把相邻重复元素移到后面,常与排序联用。

模板:

cpp 复制代码
reverse(v.begin(), v.end());
sort(v.begin(), v.end());
v.erase(unique(v.begin(), v.end()), v.end());

复杂度:

  • reverseO(n)
  • uniqueO(n),常配合排序总体 O(nlogn)

典型题型:

  • 离散化、去重、种类统计。

易错点:

  • 不排序直接 unique 只去连续重复。

推荐练习:

  • 写离散化模板时顺手练 sort + unique
lower_boundupper_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() 会出错。

推荐练习:

  • 写括号匹配基础题。
setmultiset

什么时候用:

  • 需要有序去重,或者需要保留重复但维护有序。

核心思路:

  • set 自动去重。
  • multiset 允许重复。

模板:

cpp 复制代码
set<int> s;
s.insert(3);
s.insert(1);
s.insert(3);
bool ok = s.count(1);

复杂度:

  • 插入、删除、查找通常 O(logn)

典型题型:

  • 动态判重、维护有序集合。

易错点:

  • set 不是下标数组,不支持随机访问。

推荐练习:

  • 写一个动态去重 + 输出升序结果的小程序。
mapunordered_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>
多组数据污染 vectormap 没清空 每组开始前初始化

本章练习路线

练习顺序 练习方向 目标能力 官方入口建议
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

本章自测问题

    1. 我能不看资料写比赛基础模板吗?
    1. 我知道 sort 的区间写法吗?
    1. 我知道 lower_boundupper_bound 的区别吗?
    1. 我知道什么时候该用 vector 吗?
    1. 我知道什么时候该用 set 吗?
    1. 我知道 priority_queue 默认是大根堆吗?
    1. 我知道 map[key] 会自动插入吗?
    1. 我知道 unique 后为什么还要 erase 吗?
    1. 我知道什么时候必须用 long long 吗?
    1. 我已经形成固定的下标风格了吗?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 这一章看完后,你至少要把排序、二分函数、去重、 vectormap、堆写顺手。
  • 如果你现在还经常在 STL 上卡住,不建议急着冲更难专题。
  • 最好的检验方法不是"看懂了",而是"不看文档手敲 10 分钟"。

二、枚举、模拟、排序、二分与技巧

本章适合谁

  • 会一点基础语法和 STL,但做题总觉得没有"题感"的同学。
  • 看到题目不知道该暴力、该二分、还是该模拟的人。
  • 经常把简单题写复杂,把中档题写崩的人。

建议前置知识

  • 已掌握第 1 章内容。
  • 会写基本循环和排序。

本章目标

板块 你需要达到的程度
枚举 知道什么时候暴力能过,什么时候必须优化
模拟 能把题目要求翻译成程序流程
排序 知道很多题先排一下就会好做很多
二分 会写基础二分、二分答案、边界判断
复杂度判断 能在动手前先估计是否会超时
实现技巧 能减少 WA 和 TLE

知识图谱 / 题型雷达

题目现象 优先联想
数据规模小、要求列举所有情况 枚举
题意规则多但每一步可直接按要求执行 模拟
要求先最小化 / 最大化某种代价 排序 + 贪心 或 二分答案
答案满足单调性 二分答案
给定有序数组查目标 二分查找
写法多但复杂度关键 先估 nnlognn^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)

模拟

模拟的本质

什么时候用:

  • 题目规则清晰,每一步都能按文字描述执行。

核心思路:

  • 模拟题先别急着写代码,先把流程按人类步骤列出来。

做题流程:

  1. 读题,抽取状态量。
  2. 确定每一步的更新顺序。
  3. 列清楚输入、状态、输出。
  4. 再动手写。

模板:

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_boundupper_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 内完成
单纯统计所有方案 不一定

本章自测问题

    1. 我会先看数据范围再决定做法吗?
    1. 我能区分基础二分和二分答案吗?
    1. 我知道 check 是二分答案的核心吗?
    1. 我会给模拟题先写伪代码吗?
    1. 我会在写枚举前先算复杂度吗?
    1. 我能看出一题为什么"先排序再处理"吗?
    1. 我会写左右边界二分吗?
    1. 我知道什么时候暴力就够用吗?
    1. 我会在实现复杂题前先想状态和流程吗?
    1. 我会把 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 个模板 附录

区间题速判表

题目现象 首选工具
多次区间求和 前缀和
多次区间增加 差分
矩阵子区域求和 二维前缀和
矩阵批量加值 二维差分
连续区间最短 / 最长 滑动窗口
有序数组配对 对撞指针

双指针与位运算自测

    1. 我能区分对撞指针和滑动窗口吗?
    1. 我知道滑动窗口为什么常是 O(n) 吗?
    1. 我会写前缀和区间公式吗?
    1. 我会写差分恢复原数组吗?
    1. 我知道什么时候该离散化吗?
    1. 我会写 lowbit 吗?
    1. 我知道 1LL << k 的必要性吗?
    1. 我知道前缀异或和前缀和的区别吗?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 这一章是蓝桥杯最值得反复回看的板块之一。
  • 真正掌握的标准不是"眼熟",而是你能在没有任何提示的情况下,把前缀和、差分、滑动窗口、离散化模板手敲出来。
  • 如果你只能重点看几章,这一章一定排在前列。

四、搜索专题:DFS、BFS、回溯与剪枝

本章适合谁

  • 一看到"枚举所有方案"就头皮发麻的人。
  • 会写递归,但经常爆栈、漏恢复状态、重复搜索的人。
  • 网格题、排列组合题、最短步数题做得不稳定的人。

建议前置知识

  • 已掌握前 3 章。
  • 会写基础递归和数组遍历。

本章目标

板块 目标
递归基础 理清参数、出口和转移
DFS / 回溯 会写排列、组合、子集、棋盘类搜索
BFS 会写最短步数和网格扩展
记忆化搜索 会减少重复状态
剪枝 能主动砍掉无效搜索
状态恢复 不再频繁因回溯细节 WA

知识图谱 / 题型雷达

题目现象 优先联想
枚举所有方案 DFS / 回溯
无权图最少步数 BFS
棋盘走格子 / 连通块 DFS / BFS
组合、排列、子集 回溯
同一状态会被反复求值 记忆化搜索
搜索空间很大但可提前判断无效 剪枝

递归基础

写递归之前先想什么

什么时候用:

  • 写任何 DFS、回溯、树递归之前。

核心思路:

  • 递归并不神秘,本质是"当前层做一点事,剩下交给下一层"。
  • 真正常错的不是递归本身,而是递归的定义不清楚。

你至少要回答 4 个问题:

  1. 当前函数参数表示什么。
  2. 什么时候停止。
  3. 当前层做哪些选择。
  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;

复杂度:

  • 剪枝不改变理论最坏复杂度,但通常极大提升实际效率。

典型题型:

  • 回溯、棋盘搜索、最优解搜索。

易错点:

  • 剪早了会错,剪晚了没效果。

推荐练习:

  • 把一题暴力回溯加上至少两种剪枝。
剪枝的设计顺序

什么时候用:

  • 你已经有一个正确但慢的搜索时。

核心思路:

  • 先保正确,再谈剪枝。
  • 最好先写无剪枝版本用于对拍。

建议顺序:

  1. 先写正确搜索。
  2. 找明显不合法状态。
  3. 找不可能更优的状态。
  4. 再考虑高级剪枝。

典型题型:

  • 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
小规模排列组合 回溯
同状态反复出现 记忆化搜索
搜索空间爆炸但可提前排除 剪枝

搜索自测问题

    1. 我能说清 DFS 参数表示什么吗?
    1. 我会写递归出口吗?
    1. 我知道什么时候该 BFS 吗?
    1. 我会在回溯里恢复状态吗?
    1. 我会在 BFS 里处理访问标记吗?
    1. 我知道记忆化搜索适合什么问题吗?
    1. 我知道什么时候剪枝是安全的吗?
    1. 我能区分"静态遍历"和"最短步数搜索"吗?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 搜索专题不是只靠背模板,它更考验你是否能把状态定义清楚。
  • 赛场上最容易出错的不是搜索思路,而是访问标记、恢复状态和边界判断。
  • 如果你能稳稳写出排列回溯、连通块 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 的关键不是模板,而是状态设计。

必答问题:

  1. dp[i]dp[i][j] 表示什么。
  2. 它如何从更小状态转移而来。
  3. 初始值是什么。
  4. 最终答案在哪个状态里。

模板:

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 自测清单

    1. 我能用一句中文说清 dp 的定义吗?
    1. 初始状态是哪个?
    1. 不可达状态该设成什么?
    1. 转移是从哪些更小状态来的?
    1. 答案一定在 dp[n] 吗?
    1. 能否滚动数组优化?
    1. 循环方向有没有受"是否可重复使用"影响?
    1. 这是子序列还是子数组?
    1. 是否要取模?
    1. 二维 DP 是否能降一维?
    1. 题目是求最大值、最小值还是方案数?
    1. 状态中是否漏了某个关键信息?
    1. 本题更适合递推还是记忆化搜索?
    1. 如果写暴力搜索,状态会重复吗?
    1. 区间 DP 的长度枚举顺序是否正确?
    1. 网格 DP 有没有障碍物?
    1. 是否有必要预处理前缀信息来辅助 DP?
    1. 我的 dp 数组大小够吗?
    1. 当前写法复杂度能过吗?
    1. 这个模型我以前见过哪个经典原型?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 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 开始更新导致死循环。
  • 线段树区间左右边界不统一。

图论与数据结构自测问题

    1. 我能区分邻接表和邻接矩阵的适用场景吗?
    1. 我会写并查集的 find 吗?
    1. 我知道什么时候更适合 BFS 而不是 Dijkstra 吗?
    1. 我知道拓扑排序能顺便判环吗?
    1. 我会判断最小生成树是否存在吗?
    1. 我能说出单调栈为什么是线性复杂度吗?
    1. 我会写滑动窗口最大值的单调队列吗?
    1. 我知道树状数组的核心操作为什么和 lowbit 有关吗?
    1. 我知道线段树最基础的 pushup 是什么吗?
    1. 我知道比赛里什么时候值得上线段树吗?
    1. 图是有向图还是无向图,我会在写代码前确认吗?
    1. 我会给边带权建图吗?
    1. 我知道 Dijkstra 为何要用小根堆吗?
    1. 我知道最短路题是否真的需要恢复路径吗?
    1. 我有把图题和数据结构题做分类整理吗?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 这一章内容多,但不需要一次吃完。
  • 如果你是 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 指针含义没想清楚。
  • 回文题只考虑奇数长度。
  • getlinecin >> s 混用导致读入残缺。
  • 字符串长度不大却执着上复杂模板。

字符串自测问题

    1. 我能区分字符串模拟和字符串算法题吗?
    1. 我知道什么时候该考虑 KMP 吗?
    1. 我会写 KMP 的 ne 数组吗?
    1. 我知道 KMP 成功匹配后为什么还要回退 j 吗?
    1. 我会写 Trie 的插入和查询吗?
    1. 我知道 Trie 节点数怎么估吗?
    1. 我会用前缀哈希比较子串吗?
    1. 我知道哈希为什么适合子串比较吗?
    1. 我知道最长回文的两种基础做法吗?
    1. 我能用一句话说明 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)

典型题型:

  • 分数约分、整除关系、数学构造。

易错点:

  • 有人会忘记 ab0 的边界。

推荐练习:

  • 写一个 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) % modx = (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. 我知道 1 不是素数吗?
    1. 我会写 gcdlcm 吗?
    1. 我知道 lcm 为什么要先除后乘吗?
    1. 我会写判素数吗?
    1. 我会写筛法吗?
    1. 我知道什么时候筛法比试除法更合适吗?
    1. 我会写质因数分解吗?
    1. 我知道约数个数公式吗?
    1. 我知道约数和公式的思路吗?
    1. 我会写快速幂吗?
    1. 我知道逆元常见适用条件吗?
    1. 我会写欧拉函数吗?
    1. 我知道裴蜀定理判断可行性的条件吗?
    1. 我会用 Pascal 递推求组合数吗?
    1. 我会写阶乘 + 逆元求组合数吗?
    1. 我知道取模减法为什么要防负数吗?
    1. 我知道哪些数学题其实只是模板题吗?
    1. 我有把数论题按模板归类吗?

资料延伸区

官方练习
算法阅读
接口查阅

本章收尾建议

  • 数论并不一定比图论更难,很多时候只是模板和判定条件不熟。
  • 对蓝桥杯来说,先把 gcd、筛法、快速幂、逆元、组合数这条线打牢,收益非常高。
  • 遇到数学题时不要慌,先判断它属于哪个模板族。

九、真题题型路线与备赛策略

本章适合谁

  • 不只是想"学会知识点",还想知道"该怎么练"的同学。
  • 已经学过一些专题,但不知道先刷什么、后刷什么的人。
  • 想把蓝桥杯备赛流程做得更稳定、更体系化的人。

本章怎么用

  • 如果你还没系统学专题:把本章当路线图。
  • 如果你已经会很多模板:把本章当训练计划和冲刺清单。
  • 如果你马上要比赛:重点看"赛前 7 天 / 3 天 / 1 天计划"和"比赛当天流程"。

官方资源怎么用

官方入口 1:蓝桥云课题库

链接:

适合做什么:

  • 按关键词搜索专题题。
  • 按模块补短板。
  • 做基础题和中档题。

怎么用更有效:

  1. 先在本章确定你当前最缺哪一类题。
  2. 再去题库按专题关键词搜索,而不是盲目乱刷。
  3. 每做完一个专题,至少整理 3 个固定模板。
官方入口 2:蓝桥杯真题卷

链接:

适合做什么:

  • 模拟真实比赛。
  • 看高频题型在真题里的出现方式。
  • 训练时间分配和查错节奏。

怎么用更有效:

  1. 平时可以拆题做。
  2. 冲刺阶段要整卷限时做。
  3. 做完后不要只看分数,一定要记录失分原因。
官方入口 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. 第 1 章:基础与 STL
  2. 第 3 章:前缀和、差分、双指针
  3. 第 4 章:搜索
  4. 第 5 章:线性 DP、背包
  5. 第 8 章:gcd、筛法、快速幂
  6. 第 7 章:KMP、Trie
  7. 第 6 章:并查集、 Dijkstra、拓扑
A 组补充优先级
  1. 第 6 章:树状数组、线段树
  2. 第 5 章:区间 DP、状态压缩 DP
  3. 第 7 章:Manacher
  4. 第 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 分钟 优先检查已写题,而不是开陌生大题
已经拿下多题 稳住心态,别因为贪难题把已得分写挂

赛场查错顺序

  1. 数组大小和下标。
  2. int / long long
  3. sort 和二分区间。
  4. 初始化是否漏掉。
  5. 多组数据是否清空。
  6. BFS / DFS 是否重复访问。
  7. DP 初值和答案位置。
  8. 模运算是否每步取模。

真题复盘模板

每做完一套卷,至少记录以下内容:

记录项 你要写什么
会做但写挂的题 错在边界、类型、初始化还是实现顺序
完全没思路的题 属于哪个专题,为什么没想到
花时间过长的题 卡在哪一步
提交后才发现的问题 为什么没在本地检查出来
下次行动 回看哪章、补哪类题、背哪个模板

错题分类法

类型一:思路型错误

表现:

  • 读完题不知道从哪里下手。

说明:

  • 通常是题型识别不到位。

应对:

  • 回看专题章节里的"题型识别"。
类型二:模板型错误

表现:

  • 想到了 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 件事:

  1. 今天做了哪些题。
  2. 哪一题最值得复盘。
  3. 今天学会了哪个模板。
  4. 今天最容易犯的错误是什么。
  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. 平时训练时快速回忆模板。
  2. 比赛前 1 天压缩记忆。

真正想把模板吃透,最好的方式不是反复看,而是反复手敲。

速查目录

  1. 比赛起手模板
  2. 排序、二分、去重、离散化
  3. 前缀和与差分
  4. 双指针与位运算
  5. 搜索模板
  6. 动态规划模板
  7. 图论与数据结构模板
  8. 字符串模板
  9. 数论与组合数模板
  10. 复杂度、公式与检查清单

一、比赛起手模板

基础模板
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:第一个大于等于 x
  • upper_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 分钟开一题完全陌生的大题。
  • 不要因为一题没做出来就心态崩。

十四、手敲顺序建议

如果你准备默写模板,建议按这个顺序练:

  1. 比赛基础模板
  2. 排序 + lower_bound
  3. 前缀和 + 差分
  4. 双指针
  5. DFS / BFS
  6. 0-1 背包
  7. 并查集 + Dijkstra
  8. KMP + Trie
  9. 快速幂 + 组合数

十五、比赛前的最短复习路线

如果你只剩很少时间,就按下面顺序:

  1. 看比赛基础模板。
  2. 看前缀和 / 差分 / 双指针。
  3. 看 DFS / BFS。
  4. 看背包和 LIS。
  5. 看并查集 / Dijkstra。
  6. 看 KMP / Trie。
  7. 看快速幂 / gcd / 组合数。
  8. 看最后 20 分钟检查清单。

十六、模板默写任务表

任务编号 模板名称 目标时间 合格标准
1 比赛基础模板 1 分钟 不看资料写完整
2 排序 + 二分函数 2 分钟 sortlower_boundupper_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 问

基础与实现
    1. 你能不看资料写出比赛基础模板吗?
    1. 你知道什么时候该用 long long 吗?
    1. 你会在数组题里统一下标风格吗?
    1. 你能区分 sort(a + 1, a + n + 1)sort(a, a + n) 吗?
    1. 你知道 lower_boundupper_bound 的区别吗?
    1. 你知道 unique 后还要 erase 吗?
    1. 你知道 priority_queue 默认是大根堆吗?
    1. 你知道 queue 没有 clear() 吗?
    1. 你知道 map[key] 会自动插入吗?
    1. 你知道 memset 适合初始化哪些值吗?
高频算法
    1. 你能写一维前缀和吗?
    1. 你能写二维前缀和吗?
    1. 你能写一维差分吗?
    1. 你知道差分和前缀和的关系吗?
    1. 你能写对撞指针吗?
    1. 你能写滑动窗口吗?
    1. 你知道什么时候适合双指针吗?
    1. 你会 1LL << k 吗?
    1. 你会写 lowbit 吗?
    1. 你能枚举一个集合的所有子集吗?
搜索
    1. 你能清楚定义 DFS 函数参数的意义吗?
    1. 你能写排列回溯吗?
    1. 你能写组合回溯吗?
    1. 你会做选择后恢复状态吗?
    1. 你知道什么时候应该剪枝吗?
    1. 你能写网格 DFS 吗?
    1. 你能写网格 BFS 吗?
    1. 你知道为什么最短步数更常用 BFS 吗?
    1. 你知道多源 BFS 怎么初始化吗?
    1. 你能把暴力搜索改成记忆化搜索吗?
动态规划
    1. 你知道状态定义要先写中文吗?
    1. 你能写一维线性 DP 吗?
    1. 你能写 LIS 吗?
    1. 你能写 LCS 吗?
    1. 你知道 0-1 背包为什么逆序吗?
    1. 你知道完全背包为什么正序吗?
    1. 你能写网格 DP 吗?
    1. 你知道区间 DP 的枚举顺序吗?
    1. 你知道最终答案在哪个状态里吗?
    1. 你有因为初始化写错导致 DP 挂掉过吗?
图论与字符串
    1. 你能写并查集吗?
    1. 你能写拓扑排序吗?
    1. 你能写 Dijkstra 吗?
    1. 你知道 Dijkstra 不能直接处理负边吗?
    1. 你能写 Kruskal 吗?
    1. 你会单调栈吗?
    1. 你会单调队列吗?
    1. 你会树状数组吗?
    1. 你能写 KMP 的 ne 数组吗?
    1. 你能写 Trie 的插入和查询吗?
数论与比赛
    1. 你能写判素数吗?
    1. 你能写筛法吗?
    1. 你能写质因数分解吗?
    1. 你能写快速幂吗?
    1. 你知道费马小定理常见适用条件吗?
    1. 你能写组合数的 Pascal 递推吗?
    1. 你能写阶乘 + 逆元求组合数吗?
    1. 你有自己的最后 20 分钟检查顺序吗?
    1. 你知道比赛里该先做哪些题吗?
    1. 你知道自己最容易错的 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 条高频错点

    1. sort 右端点少写一位。
    1. lower_bound 用在无序区间。
    1. unique 后忘记 erase
    1. 1 << 40 没写成 1LL << 40
    1. 前缀和数组开成了 int
    1. 差分的 r + 1 越界。
    1. 双指针数组没排序。
    1. 滑动窗口条件写反。
    1. DFS 没写出口。
    1. 回溯忘记恢复状态。
    1. used 数组多组数据没清空。
    1. BFS 没在合适时机标记访问。
    1. BFS 起点没初始化距离。
    1. DFS / BFS 坐标体系不统一。
    1. 记忆化搜索的"未计算标记"没统一。
    1. DP 状态定义自己都说不清。
    1. DP 初始值错误。
    1. 0-1 背包循环方向写成正序。
    1. 完全背包循环方向写成逆序。
    1. LIS 把子序列当子数组。
    1. 区间 DP 的长度枚举顺序错。
    1. 网格 DP 忘记处理障碍。
    1. Dijkstra 用在负边图上。
    1. 堆里旧状态没过滤。
    1. 并查集忘记初始化。
    1. Kruskal 做完没判连通。
    1. 拓扑排序没检查是否存在环。
    1. 单调栈存值还是存下标没分清。
    1. 单调队列窗口过期条件错。
    1. 树状数组从 0 开始更新。
    1. 线段树左右孩子区间写错。
    1. 线段树空间没开 4n
    1. KMP 的 j 回退写错。
    1. KMP 成功匹配后没继续回退。
    1. Trie 节点数估小。
    1. 字符集不是 26 还写死 26
    1. 哈希数组下标不统一。
    1. 回文题漏写偶数中心。
    1. getline 被残留换行影响。
    1. 把简单模拟题写成重型字符串算法题。
    1. gcd / lcm 边界没考虑 0
    1. lcm 先乘后除溢出。
    1. 1 当成素数。
    1. 试除法循环上界写错。
    1. 筛法数组空间没估够。
    1. 质因数分解漏掉最后的大素因子。
    1. 快速幂忘记每步 % mod
    1. 逆元在不适用的模数上乱用。
    1. 欧拉函数公式记混。
    1. 裴蜀定理只会背不会判条件。
    1. Pascal 递推边界没初始化。
    1. 阶乘逆元预处理终点错。
    1. 模减法忘记防负数。
    1. 多组数据没清空答案变量。
    1. vector 越界访问。
    1. 空栈访问 top()
    1. 空队列访问 front()
    1. map[key] 自动插入导致逻辑变化。
    1. priority_queue 方向反了。
    1. 自定义比较器关键字顺序写反。
    1. 图是有向还是无向没确认。
    1. 边权和距离还用 int
    1. 样例过了就直接交,没有手造边界。
    1. 最后答案位置看错。
    1. 复杂度只看单次,没看总次数。
    1. 多次查询每次都重新排序。
    1. 递归深度太深没防爆栈。
    1. 需要 long long 的地方偷懒用 int
    1. 数组大小卡得太死。
    1. memsetlong long 设大值。
    1. 二分边界不收缩导致死循环。
    1. 二分答案没有单调性还强上。
    1. check 函数写对了吗没有单独验证。
    1. 离散化漏收集查询值。
    1. 题目要的是方案数却写成最优值。
    1. 题目要的是最小值却用 max
    1. 列表 / 路径输出顺序没确认。
    1. 真题模拟时一题卡太久不跳。
    1. 最后 20 分钟没检查已做题。
    1. 没有自己的错题分类体系。

二十四、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
    1. 字符串长度为 1
    1. 图只有一个点。
    1. 图没有边。
    1. 图不连通。
    1. 所有数组元素相同。
    1. 所有数组元素严格递增。
    1. 所有数组元素严格递减。
    1. 所有边权相同。
    1. 所有边权都很大。
    1. 起点就是终点。
    1. BFS 起点四周全是障碍。
    1. 滑动窗口长度为 1
    1. 滑动窗口长度等于 n
    1. 二分答案就在左边界。
    1. 二分答案就在右边界。
    1. mod = 1
    1. a = 0b = 0
    1. gcd(a, b) = 1
    1. gcd(a, b) = a
    1. 组合数 m = 0
    1. 组合数 m = n
    1. 组合数 m > n
    1. Trie 中只有一个单词。
    1. Trie 查询不存在的单词。
    1. KMP 主串和模式串完全相同。
    1. KMP 模式串只有一个字符。
    1. 哈希比较同一段子串。
    1. 回文串全相同字符。
    1. 回文串完全没有长度大于 1 的回文。
    1. 并查集中重复合并同一对点。
    1. Kruskal 边数不足。
    1. 拓扑图有环。
    1. 树状数组修改最后一个点。
    1. 树状数组查询前缀 0
    1. 线段树区间只剩一个点。
    1. 前缀和查询整个区间。
    1. 差分修改只改一个点。
    1. 双指针找不到解。
    1. LIS 全是相等元素。
    1. LCS 两串完全不同。
    1. 背包容量比所有物品都小。
    1. 背包容量恰好装满。
    1. 所有价值为 0
    1. DP 初始状态就是最终答案。
    1. 搜索只有一条路径。
    1. 搜索分支极多但早剪枝。
    1. 多组数据第一组和第二组规模差很大。
    1. 输入有负数。
    1. 输出要求取模但答案可能先变负。

二十六、60 条赛场口令

    1. 先看范围,再想算法。
    1. 先做稳题,不先碰最难题。
    1. 样例过了不等于题过了。
    1. 一题卡太久要敢跳。
    1. 写前先确认下标体系。
    1. 写前先确认类型范围。
    1. 二分先想单调性。
    1. 搜索先想状态和出口。
    1. DP 先写中文状态定义。
    1. 图题先确认有向还是无向。
    1. 最短步数优先想 BFS。
    1. 非负最短路优先想 Dijkstra。
    1. 区间和优先想前缀和。
    1. 区间修改优先想差分。
    1. 连续区间优先想双指针。
    1. 多次组合数查询优先想预处理。
    1. 字符串匹配优先判断要不要 KMP。
    1. 字符串集合优先想 Trie。
    1. 模幂题先把快速幂写出来。
    1. 取模减法先防负数。
    1. lcm 先除后乘。
    1. 复杂度要看总次数。
    1. 排序后很多题会简单很多。
    1. unique 后别忘 erase
    1. priority_queue 默认是大根堆。
    1. map[key] 会自动插入。
    1. queue 没有 clear()
    1. memset 不是万能初始化。
    1. 1LL << k1 << k 稳。
    1. 滑动窗口要明确何时收缩。
    1. 回溯固定写"做选择 -> 递归 -> 撤销"。
    1. BFS 通常入队就标记。
    1. 并查集每组数据都要重置。
    1. 树状数组最好用 1 下标。
    1. 线段树空间先开 4n
    1. Dijkstra 不是万能最短路。
    1. Kruskal 做完要检查是否连通。
    1. 拓扑排序能顺便判环。
    1. LIS 是子序列,不是子数组。
    1. 背包方向一错,全题都错。
    1. 0-1 背包逆序。
    1. 完全背包正序。
    1. 组合回溯要靠 start 去重。
    1. 排列回溯要靠 used 去重。
    1. 看到"最小的最大值"想二分答案。
    1. 看到"互质个数"想欧拉函数。
    1. 看到"ax + by = c"想裴蜀定理。
    1. 看到"前 i 个"常想线性 DP。
    1. 看到"每组选一个"想分组背包。
    1. 看到"依赖顺序"想拓扑排序。
    1. 看到"当前窗口最值"想单调队列。
    1. 看到"最近更小 / 更大"想单调栈。
    1. 看到"动态连通"想并查集。
    1. 看到"值域大但值少"想离散化。
    1. 最后 20 分钟优先查已做题。
    1. 不要临场发明新模板。
    1. 不要轻易换自己熟悉的风格。
    1. 不要把会做的题写挂。
    1. 题没做出也别乱心态。
    1. 赛后一定要复盘错误类型。

二十七、50 条模板提醒

    1. 比赛模板越短越好。
    1. 常量写在最前面更稳。
    1. ios::sync_with_stdio(false); 几乎默认要写。
    1. 排序和二分最好配套记忆。
    1. 前缀和默认开 long long
    1. 差分数组多开一位。
    1. 二维数组边界要比一维更小心。
    1. 双指针数组有序时更常见。
    1. 快慢指针不只用于链表。
    1. 子集枚举只适合小 n
    1. DFS 参数意义一定要清楚。
    1. BFS 队列里的元素结构要先设计好。
    1. 记忆化搜索的"未访问标记"要统一。
    1. 背包模板手感比记忆更重要。
    1. LIS 的 dp[i] 是"以 i 结尾"很常见。
    1. 网格 DP 起点初始化必须单独看。
    1. 区间 DP 先枚举长度。
    1. 并查集 find 建议直接路径压缩。
    1. Dijkstra 需要小根堆。
    1. 边权和距离尽量开 long long
    1. Kruskal 的边要先排序。
    1. 单调栈通常存下标更灵活。
    1. 单调队列也常存下标。
    1. 树状数组最核心就是 lowbit
    1. 线段树先会 buildpushup
    1. KMP 先背 ne,再背匹配。
    1. Trie 的节点总数要按总字符量估计。
    1. 哈希最好统一 1 下标。
    1. 回文中心扩展要写奇偶两种。
    1. 快速幂每一步都要 % mod
    1. 判素数时 x < 2 直接假。
    1. 质因数分解别漏最后的大素因子。
    1. 组合数 m > n 直接是 0。
    1. 逆元模板要先确认模数条件。
    1. gcd(a, 0) 这种边界别忘。
    1. lcm 先除后乘不是形式主义。
    1. 离散化后编号从 0 还是 1 要统一。
    1. 多组数据题清空比重写更重要。
    1. 容器题要先想是否需要顺序。
    1. unordered_map 不一定总比 map 好。
    1. 样例少时更要自己造边界。
    1. 模板写得太花哨反而容易出错。
    1. 比赛中模板应尽量用自己练过的版本。
    1. 不熟的模板宁可少写,也别半懂硬上。
    1. 抄模板前先知道它解决什么问题。
    1. 题型不对,模板越标准也没用。
    1. 模板只是骨架,变量意义要看题改。
    1. 赛前至少整体默写一次附录。
    1. 错题要反推到模板哪一行不熟。
    1. 真正掌握模板的标准是不看资料能写出 80%。

二十八、20 组题型信号速读

    1. "区间和很多" -> 前缀和。
    1. "区间修改很多" -> 差分。
    1. "最短步数" -> BFS。
    1. "所有方案" -> 回溯。
    1. "非负最短路" -> Dijkstra。
    1. "动态连通" -> 并查集。
    1. "有依赖关系" -> 拓扑排序。
    1. "连接所有点最小代价" -> Kruskal。
    1. "连续区间最值" -> 滑动窗口。
    1. "两端夹逼" -> 对撞指针。
    1. "模式匹配" -> KMP。
    1. "词典管理" -> Trie。
    1. "子串比较" -> 哈希。
    1. "最长回文" -> 中心扩展 / Manacher。
    1. "选物品" -> 背包。
    1. "前 i 个最优" -> 线性 DP。
    1. "互质个数" -> 欧拉函数。
    1. "模幂" -> 快速幂。
    1. "约数个数" -> 质因数分解。
    1. "组合数多次查询" -> 阶乘 + 逆元。

二十九、补充默写框架

组合回溯空框
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 条查错问题

    1. 数组是不是开小了?
    1. 下标是不是混用了 0 和 1?
    1. sort 区间真的写对了吗?
    1. 二分区间真的有序吗?
    1. 二分更新方向会不会死循环?
    1. check 函数真的单调吗?
    1. long long 用够了吗?
    1. 中间乘法是不是先溢出了?
    1. 前缀和公式有没有写反?
    1. 差分恢复有没有漏原数组?
    1. 滑动窗口收缩条件正确吗?
    1. DFS 出口写了吗?
    1. 回溯恢复状态了吗?
    1. BFS 访问标记时机对吗?
    1. 多组数据清空了吗?
    1. DP 初始值合理吗?
    1. DP 答案位置找对了吗?
    1. 背包循环方向对吗?
    1. 图边都加全了吗?
    1. 有向图误加反向边了吗?
    1. Dijkstra 图里有负边吗?
    1. Kruskal 是否连通了?
    1. 树状数组是不是从 1 下标开始?
    1. 线段树孩子区间划分对吗?
    1. KMP 的 j 回退写对了吗?
    1. Trie 字符集对应对了吗?
    1. 哈希下标统一了吗?
    1. 质因数分解最后剩余值处理了吗?
    1. 快速幂每步都 % mod 了吗?
    1. 逆元适用条件满足吗?
    1. 组合数 m > n 处理了吗?
    1. 模减法防负数了吗?
    1. 输出的是题目真正要的答案吗?
    1. 边界样例自己造了吗?
    1. 极小值样例试了吗?
    1. 极大值样例试了吗?
    1. 所有元素相同试了吗?
    1. 起点终点重合试了吗?
    1. 只剩一个元素的情况试了吗?
    1. 提交前有没有从头顺一遍逻辑?

三十一、50 条复盘口令

    1. 只看答案不算复盘。
    1. 只看题解不算补题。
    1. 会做但写挂最值得复盘。
    1. 没想到题型要回章节补模型。
    1. 想到了模板却写不顺,要练默写。
    1. TLE 先看复杂度,不先怪编译器。
    1. WA 先看边界,不先怀疑平台。
    1. RE 先看数组和越界。
    1. 多组数据错先看初始化。
    1. 二分错先看单调和边界。
    1. 搜索错先看状态恢复。
    1. DP 错先看状态定义。
    1. 图题错先看建边方式。
    1. 字符串错先看题型判断。
    1. 数论错先看模板适用条件。
    1. 错题要写"为什么错",不是只记题号。
    1. 一类题错两次,就该单独建专题。
    1. 会做但超时,也算没拿到分。
    1. 不会做的题要归类,不要只是沮丧。
    1. 复盘后的动作必须具体。
    1. 明天补什么要写成一句话。
    1. 复盘时优先找共性错误。
    1. 模板题做错,问题往往不是题目难。
    1. 错因要写到"哪一行可能出错"。
    1. 真题复盘比单题复盘更能看出节奏问题。
    1. 模拟赛后先复盘,不先开新卷。
    1. 如果总卡在实现,说明基础不稳。
    1. 如果总卡在题型判断,说明模型不熟。
    1. 如果总卡在时间分配,说明比赛策略要练。
    1. 复盘不是否定自己,是为了下次更快。
    1. 错题二刷比新题乱刷更有价值。
    1. 记住"哪里浪费了时间"。
    1. 记住"哪题本来该拿到"。
    1. 记住"为什么没敢跳题"。
    1. 记住"为什么明知会做却没写出来"。
    1. 每周至少整理一次复盘表。
    1. 一道题至少能归到一个章节。
    1. 如果归不到章节,说明体系还不够清楚。
    1. 复盘最终要回到模板和判断力。
    1. 复盘后要有一次再做验证。
    1. 不验证的复盘容易变成感想。
    1. 能一句话说出错因,说明你真的理解了。
    1. 能避免第二次再犯,复盘才算有效。
    1. 不要把"我粗心了"当最终结论。
    1. 粗心往往背后是流程缺失。
    1. 没有检查顺序,就容易反复粗心。
    1. 模板越熟,复盘越能聚焦到真正问题。
    1. 题目做对了,也要想有没有更稳写法。
    1. 题目做快了,也要想有没有更短模板。
    1. 最终目标不是刷更多题,而是拿更稳定的分。

三十二、补充默写框架

前缀计数空框
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 条时间分配提醒

    1. 开局先扫题。
    1. 第一题不要超过你的预设时间。
    1. 稳题先拿分。
    1. 中档题要设止损时间。
    1. 卡住就记状态后跳题。
    1. 不要反复从头读同一题。
    1. 会做但实现长的题先评估值不值。
    1. 最后 30 分钟优先检查。
    1. 最后 10 分钟别开新大题。
    1. 一题 AC 后先稳住情绪。
    1. 两题都卡时先保最熟的。
    1. 不要被别人节奏影响。
    1. 不要因为一题失利放弃整场。
    1. 已拿的分比未知分更重要。
    1. 真题模拟时就要练这个节奏。
    1. 节奏也是能力的一部分。
    1. 会做但没时间写完,等于没拿到。
    1. 每场赛后都要复盘时间线。
    1. 你要知道自己哪类题最耗时。
    1. 时间分配不稳定,分数就不稳定。

三十四、15 条赛后动作提醒

    1. 当天整理错题类型。
    1. 第二天重做最可惜的题。
    1. 一周内再做一次同类题。
    1. 把新学到的模板收进附录。
    1. 删掉自己不会用的花哨写法。
    1. 对高频错误建立个人清单。
    1. 给每个章节留出"补弱题位"。
    1. 模拟赛和专题题要交替进行。
    1. 模板会写不代表比赛一定会用。
    1. 要把"想到"训练成"写出"。
    1. 要把"写出"训练成"写稳"。
    1. 要把"写稳"训练成"写快"。
    1. 每次复盘都要有下一步动作。
    1. 只做记录没有行动不算升级。
    1. 真正的进步来自长期重复。

结语

最后还是那句话:比赛中最怕的不是不会,而是本来会做却因为模板不熟、边界不稳、时间分配失衡而丢分。希望这篇文章能帮你把"会一点"变成"会得稳"。

回到顶部

相关推荐
历程里程碑2 小时前
二叉树---二叉树的最大深度
大数据·数据结构·算法·elasticsearch·搜索引擎·全文检索·深度优先
自我意识的多元宇宙2 小时前
树与二叉树--树的基本概念
数据结构·算法
吃着火锅x唱着歌2 小时前
LeetCode 678.有效的括号字符串
算法·leetcode·职场和发展
不爱吃炸鸡柳3 小时前
手撕哈希表(Hash Table):从原理到C++完整实现
c++·哈希算法·散列表
charlie1145141913 小时前
通用GUI编程技术——图形渲染实战(三十一)——Direct2D效果与图层:高斯模糊到毛玻璃
c++·图形渲染·gui·win32
音视频牛哥3 小时前
鸿蒙 NEXT RTSP/RTMP 播放器如何回调 RGB 数据并实现 AI 视觉算法分析
人工智能·算法·harmonyos·鸿蒙rtmp播放器·鸿蒙rtsp播放器·鸿蒙next rtsp播放器·鸿蒙next rtmp播放器
飞Link3 小时前
掌控 Agent 的时空法则:LangGraph Checkpoint (检查点) 机制深度实战
开发语言·python·算法
乐迪信息3 小时前
智慧港口中AI防爆摄像机的船舶越线识别功能
大数据·人工智能·物联网·算法·目标跟踪
自信150413057593 小时前
重生之从0开始学习c++之内存管理
c++·学习