这节课不扩新知识,而是建立暑期训练的第一套读题工作流。拿到题直接敲代码的习惯会让你在 CSP-J T3/T4 上翻车。
L01 暑期诊断 + 算法底座
先建第一套读题工作流:数据范围 → 估复杂度 → 识别模型 → 选模板 → 造边界数据。不是背更多算法,是"有没有活路"的判断力。
一、先回答问题
蛋*:搜索 / 背包 / 模拟 / 读题边界,把基础题写稳。
丁*:图论建模 / DP 分类 / 复杂度 / 对拍,提高题写快写准。
| 问题 | 课堂答案 |
|---|---|
| 拿到题先做什么? | 先看数据范围和问题目标,不急着写代码。 |
| 数据范围有什么用? | 反推可接受复杂度,排除不可能的算法。 |
| 为什么不能随便贪心? | 贪心必须证明局部选择不会破坏全局最优。 |
| 为什么要做诊断? | 暑期计划必须按真实短板分层,不按"听过什么"判断。 |
二、本节知识点地图
| 知识点 | 要达到的程度 | 课堂检查 |
|---|---|---|
| 数据范围反推复杂度 | 看到 n 能排除明显不可能的算法 | 口答 6 个数据范围 |
| 常见复杂度量级 | 知道 O(n!)、O(2^n)、O(n^3)、O(n log n) 的差别 | 复杂度排序练习 |
| 模型识别 | 能从题面信号判断模拟、搜索、二分、图论、DP | 模型卡片匹配 |
| 贪心警惕 | 知道"看起来最优"不等于正确 | Forming Quiz Teams 反例讨论 |
| 错因分类 | 不再写"粗心",能归因到 6 类错误 | 错因表现场填写 |
| 诊断复盘 | 能给自己定第一周补救目标 | 课后 3 分钟自评 |
三、概念讲解
复杂度不是让你背定义,而是让你在写代码前先判断"这个方法有没有活路"。
如果一个算法需要做 1012 次操作,哪怕代码写得漂亮,也不可能在 1 秒内跑完。
题目表面是故事,算法题真正考的是背后的模型。每道题都写一句:
这题的模型是 ______,因为题面出现了 ______。
错误说法:"每次选最近的,看起来总和就会最小。"
正确追问:"你这一步选了以后,后面的选择空间有没有被破坏?有没有一个反例能让它失败?"
"粗心"无法指导训练。
错因写"边界" → 补 3 组边界数据;写"模板" → 默写模板;写"复杂度" → 重选算法。
| 循环层数 | 工作量直觉 |
|---|---|
| 一层循环 | 走一遍所有数据 |
| 两层循环 | 每个数都和每个数配对 |
| 三层循环 | 每对关系还要再枚举一个中间点 |
| 递归枚举 | 分叉树,增长很快 |
四、思维过程:复杂度天梯表
一秒大约承受 108 量级的简单操作(粗略估算,帮助排除明显不可能的算法)。
| 数据范围 | 常见可尝试复杂度 | 可以想到 | 不能乱用 |
|---|---|---|---|
n <= 10 | O(n!)、小规模 DFS | 全排列、搜索树 | 更复杂算法不一定值得 |
n <= 20 | O(2^n)、O(n·2^n) | 子集枚举、状态压缩 | 20! 不可行 |
n <= 500 | O(n^3) | Floyd、区间 DP | 四重循环危险 |
n <= 5000 | O(n^2) | LIS/LCS、二维 DP | 三重循环通常危险 |
n <= 1e5 | O(n log n) | 排序、堆、二分、Dijkstra | 双重循环通常 TLE |
n <= 1e6 | O(n) 或接近线性 | 前缀和、双指针、线性筛 | O(n log n) 要看时限 |
n >= 1e9 | O(log n)、O(1)、O(√n) | 数学、快速幂、二分 | 逐个枚举不可能 |
10! = 3,628,800,而 20! 已经远超可接受范围。
但 220 = 1,048,576 ≈ 1e6,完全可以做。
所以 n <= 20 常暗示"选或不选""状压 DP",而非全排列。
Floyd 是三重循环:5003 = 125,000,000 ≈ 1.25e8,C++ 中可候选(还要看时限和常数)。
但若 n=5000:50003 = 1.25e11,明显不合适。
五、模型识别:不要猜算法
读题不是把文字读完,而是找"信号"。
| 题面信号 | 先想到 | 第一问 |
|---|---|---|
| 按规则一步步变化 | 模拟 | 状态和操作是什么? |
| 求所有方案 / 连通块 / 最短步数 | 搜索 | 状态是什么?如何判重? |
| 最大化最小值 / 最小化最大值 | 二分答案 | check(x) 是否单调? |
| 点、边、路径、依赖 | 图论 | 点和边分别是什么? |
| 最优值 / 方案数 / 子问题重复 | DP | 状态、转移、边界、顺序是什么? |
| 故事 | 可能模型 |
|---|---|
| 机器内存放单词 | 队列模拟 |
| 湖中水域连成片 | Flood Fill |
| 采药有时间和价值 | 0-1 背包 |
| 石头之间跳跃距离 | 二分答案 |
| 城市之间道路 | 图论最短路 |
六、贪心反例:Forming Quiz Teams
有 2N 个人,每人有坐标,要把他们两两配对,使所有配对距离之和最小。
学生直觉:每次找当前距离最近的两个人配成一组。
4 个点 A、B、C、D:A-B 很近,但配了 A-B 后,C-D 特别远。
也许 A-C + B-D 的总和反而更小。
结论:局部最近,不一定全局最短。贪心必须能证明。
错误直觉(贪心)→ 正确但太慢(枚举所有配对)→ 状态压缩 DP(二进制记录谁已配对)。这是很多竞赛题的真实成长路径。本节只要求建立警惕,不要求掌握状压 DP。
七、互动演示:选数据范围 → 判断算法
每次切换挡位,右侧显示手推计算过程。
八、六类错因:不要再说"粗心"
| 错因 | 典型表现 | 订正动作 |
|---|---|---|
| 读题 | 条件漏看、输出格式错 | 复述题意,标关键词 |
| 边界 | n=1、全相同、最大值错 | 自造 3 组边界 |
| 模板 | 背包、DFS、Dijkstra 写不出 | 默写模板 |
| 复杂度 | 暴力 TLE、数组爆内存 | 写出量级 |
| 调试 | 样例过但 WA/RE | 打印、GDB、对拍 |
| 实现 | 下标、初始化、循环方向错 | 写个人 checklist |
"粗心"不是错因。粗心背后一定是读题、边界、模板、复杂度、调试或实现中的一个具体问题。以后错题必须归到 6 类之一。
九、例题精讲
P1596 湖计数(Flood Fill)
网格、水域(W)、相邻(8 方向)、连成一片算同一片湖,问共有几片湖。
Flood Fill(连通块计数)。每次遇到未访问的 W,从它出发把整个连通块染色,计数 +1。
| 检查点 | 容易错在哪 |
|---|---|
| 方向数组 | 是 8 个方向(含对角线),不是 4 个 |
| 越界判断 | 行列下标都要判,放在访问前 |
| visited 时机 | 进入 DFS 即标记,防止同一块被重复计数 |
| 计数逻辑 | 一个连通块只在第一次发现时计数 +1,内部递归不再计数 |
P1048 采药(0-1 背包)
每株药有时间(重量)和价值,每株只能采一次,总时间有限(容量),求最大价值。
0-1 背包。每件物品选或不选,dp[j] = 时间恰好为 j 时可获得的最大价值。
| 检查点 | 容易错在哪 |
|---|---|
| dp[j] 含义 | 剩余时间为 j 时能取到的最大价值 |
| 容量倒序 | 必须从大到小枚举 j,否则同一株药会被选多次(变成完全背包) |
| 初值 | dp[0..T] = 0,表示什么也不选时价值为 0 |
| 时间超限 | 若某株药时间 > T,直接跳过(j 从 T 到 w[i] 的范围不会执行) |
C++14 核心代码:P1048 采药(0-1 背包)
#include <bits/stdc++.h>
using namespace std;
int T, M; // T: 总时间, M: 草药数量
int t[1005], v[1005]; // t[i]: 采药时间, v[i]: 价值
int dp[1005]; // dp[j]: 用时恰好为 j 时的最大价值
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> T >> M;
for (int i = 1; i <= M; i++)
cin >> t[i] >> v[i];
// 0-1 背包:物品循环在外,容量倒序在内
for (int i = 1; i <= M; i++) {
for (int j = T; j >= t[i]; j--) { // 倒序!防止一株药选多次
dp[j] = max(dp[j], dp[j - t[i]] + v[i]);
}
}
cout << dp[T] << "\n";
return 0;
}
读代码重点:倒序枚举容量是 0-1 背包的核心,正序枚举是完全背包。两种只差一个方向,含义完全不同。
十、当堂习题
互动选择题
n = 20,每个物品可以选或不选。合理的复杂度?
看到"每个物品只能选一次,总容量有限,求最大价值",先想到?
0-1 背包里为什么容量要倒序枚举?
错题归因,下面哪个写法是正确错因?
分层作业表
| 学生 | 题目 | 要求 |
|---|---|---|
| 蛋* | P1596 湖计数 | 独立 AC,说清连通块只计一次,自造 3 个边界 |
| 蛋* | P1048 采药 | 独立 AC,解释为什么容量倒序 |
| 丁* | P4779 单源最短路 | 默写 Dijkstra 结构,解释堆优化 |
| 丁* | P1880 石子合并 | 写出状态、转移、枚举顺序,处理环形 |
| 全员 | 错因表 | 今天每个错误必须归到 6 类之一 |
十一、诊断安排
| 内容 | 时间 | 目的 |
|---|---|---|
| 选择判断 A 组 | 20 分钟 | 看基础概念和复杂度反应 |
| 编程诊断 | 45 分钟 | 看模板熟练度和代码稳定性 |
| 自评 | 5 分钟 | 让学生说出自己的卡点 |
P1596 湖计数:Flood Fill、边界、visited。
P1048 采药:0-1 背包、逆序循环。
P4779 单源最短路:Dijkstra、堆、建图。
P1880 石子合并:区间 DP、环形处理。
十二、课堂验收
1. n <= 20 为什么常想到 2^n?
2. n <= 500 为什么可能用 Floyd?
3. 为什么贪心不能只凭直觉?
4. P1596 是什么模型?检查点有哪些?
5. P1048 是什么模型?为什么容量倒序?
6. 错因为什么不能写"粗心"?