说句实话,复习到第9天是一个非常尴尬的节点。往前翻,前八天的热情和新鲜感已经消耗得差不多;往后看,离复试机试还有一段距离,但绝对没到可以慢悠悠打基础的时候。如果你也在准备408相关的机试复试,尤其是冲着复旦这种档位的学校去,这种感觉会更明显:既要担心408四门课的基础被忘掉,又怕机试编程到考场上写不出东西。
这篇东西不是给你灌鸡汤的,是我自己Day9这一整天的真实复盘,包含机试到底考什么、408里哪些内容和机试强相关、当天具体刷了什么题、踩了什么坑、以及接下来几天的调整方向。不管你现在是Day1还是Day20,只要目标机试涉及数据结构和算法,这篇都能直接拿来参考。
1. 机试到底在考什么:从408知识版图里划出真正的得分主力
很多人准备复试机试有个误区,就是把408四门课当成一个整体去平均用力。实际上,机试的考察范围远没有笔试那么宽,它基本只关心你"能不能用代码解决一个明确的问题",所以四门课的优先级完全不一样。
1.1 数据结构与算法:机试当之无愧的绝对主角
机试的题目再怎么变,核心都逃不开数据结构加算法。链表、栈、队列、二叉树、图、排序、查找、动态规划、贪心、回溯,这些都是高频考点。408里的数据结构部分,本质上就是机试的"题库说明书",你在笔试里背过的那些复杂度结论、遍历顺序、平衡调整,到了机试全都要变成能跑的代码。
我的建议是,从Day9开始,每天都要保证有一半以上的时间花在数据结构和算法题上。不是泛泛地刷,而是按专题推进。比如今天定的是动态规划和图论,那就一天之内把同一类题做透,形成一个条件反射式的思维套路。
1.2 操作系统、计组、网络的机试权重:别把力气花错地方
这三门课在机试里不是不考,而是考法非常隐蔽。
操作系统最可能出现的形态是"进程同步/互斥"模拟题,说白了就是拿代码模拟信号量。偶尔也会出读者写者问题、生产者消费者问题的变种,考的是你能不能写出无死锁的逻辑。计算机组成原理在机试里很少直接出题,但它的价值体现在"隐性坑"里——比如整数溢出、移位运算、浮点精度。很多算法题你以为自己逻辑对了,结果就是栽在int和long long上,这其实就是计组的知识。计算机网络就更边缘了,最多出一些报文解析、滑动窗口模拟之类的题目,但频率极低,不需要花大块时间。
我把四门课的机试权重整理成了一个表,方便你在第二轮的复习规划里做资源分配。
| 408科目 | 机试直接出题概率 | 常见机试形态 | 投入建议 |
|---|---|---|---|
| 数据结构 | 极高 | 树、图、栈、堆、排序、哈希 | 每天必练,按专题推进 |
| 算法(大纲内含于数据结构) | 极高 | DP、贪心、回溯、搜索剪枝 | 每天必练,独立成专题 |
| 操作系统 | 中低 | 进程同步模拟、调度算法实现 | 理解PV操作即可,不需要深挖内核 |
| 计算机组成原理 | 低 | 极少直接出题,但溢出、补码、移位常做背景坑 | 做题时注意数据范围和位运算,不单独复习 |
| 计算机网络 | 低 | 报文格式、滑动窗口模拟 | 查漏用,不主动投入整块时间 |
想明白这一点,Day9的复习就不会四门课平均用力。上午花时间巩固408笔试内容,下午整个黄金时段全部让给机试刷题,晚上再回过头来补白天的漏洞。
1.3 机试的评分逻辑:部分样例、隐藏样例与策略性"骗分"
机试判题和平时在在线评测系统上刷题一样,不是只看你最后跑没跑通,而是有一组测试样例。公开样例一般会在题目里给出来,那是帮你验证基本思路的。真正决定分数的是隐藏样例,专门卡边界条件、大数据量、特殊输入。
所以Day9开始,我做每一道题都强迫自己干一件事:除了提交正确代码,还要刻意去构造极端数据。数组有没有开到足够大?循环有没有处理空集?数值会不会溢出?字符串里有没有空格?这些都是在隐藏样例里最常见的杀手。
另外一个务实的策略是"暴力分"。有些题目的数据范围给了两档,小数据用暴力法能过一部分测试点,大数据才需要正解。如果考场上15分钟没想到最优解,我绝不会死磕,先写一版暴力,保证至少拿到部分分,然后再去优化。这个习惯在你平时练习时就要养成,否则考场上一定会慌。
2. Day9怎么安排:从刷模板题切换到专题突破的黄金节点
前八天大概率是在过基础模板,比如实现各种排序、链表操作、二叉树遍历。Day9这个时间点,基础模板应该已经形成肌肉记忆了,再刷模板题属于浪费时间。这一天,我开始强制自己进入"专题突破+真题模拟"的混合模式。
2.1 当天整体时间轴:给408笔试留窗口,给机试留大块时间
我这一天的安排是这样拆的,你可以直接抄:
- 08:30-09:30 数据结构笔试点:平衡二叉树、红黑树性质回顾。不写代码,纯推导旋转和变色逻辑
- 09:30-10:30 计算机组成原理:补码、溢出判断。重点看无符号数和有符号数的运算陷阱,这部分虽然机试不直接考,但写代码时避免溢出就靠它
- 10:30-11:30 操作系统:信号量PV操作,做两道同步互斥的模拟题
- 14:00-16:00 机试专项:动态规划3题 + 图论2题,全程限时,模拟考场压力
- 16:30-18:00 复盘:把没AC的题目重新调通,分析卡住的环节
- 20:00-21:30 整理模板、构造边界测试数据、规划Day10的专题
上午的时间其实是给408笔试兜底的。因为机试只是复试的一部分,复试还有笔试或者面试环节,408四门课的基础不能丢。但必须控制时间,上午三个小时足够,绝不能让笔试复习挤占下午的机试黄金时段。
2.2 为什么Day9选"动态规划+图论"作为主攻方向
动态规划和图论是机试里最有区分度的两个专题。树和链表的题,大家都能写;但一旦上DP和图论,正确率立刻分层。Day9不是刚开始接触这两个专题,而是到了该把"会做模板题"升级成"能做中等难度综合题"的阶段。
动态规划我喜欢用三个递进层次来练:第一层是状态定义,第二层是状态转移方程,第三层是边界和滚动数组优化。前八天如果已经把01背包、完全背包、最长上升子序列的裸题搞定了,那Day9就要开始碰区间DP、树形DP这种更绕的东西。图论这边也是一样,Dijkstra、Floyd、并查集、最小生成树这些最基础的算法必须闭着眼写出来,然后才能去碰次短路、分层图、拓扑排序加动态规划这种综合题。
2.3 一个典型的区间DP例题:从状态定义到AC全过程
今天做的动态规划里,我觉得"石子合并"这道题最值得拿出来讲。题目描述很简单:有一排石子堆,每堆石子有重量,每次只能合并相邻两堆,代价是两堆重量之和,问把所有石子合并成一堆的最小总代价。
这是一个非常经典的区间DP入门题,很多人一开始会误以为贪心能解,实际上贪心只能解环形且可以任意合并的情况,而"只能合并相邻"必须DP。
定义状态dp[i][j]表示合并第i堆到第j堆石子的最小代价。答案就是dp[1][n]。状态转移方程是dp[i][j] = min(dp[i][j], dp[i][k] + dp[k+1][j] + sum(i, j)),其中k从i遍历到j-1。
#include <bits/stdc++.h> using namespace std; const int MAXN = 305; const int INF = 0x3f3f3f3f; int n; int a[MAXN], pre[MAXN]; int dp[MAXN][MAXN]; int getSum(int l, int r) { return pre[r] - pre[l - 1]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n; for (int i = 1; i <= n; i++) { cin >> a[i]; pre[i] = pre[i - 1] + a[i]; } for (int i = 1; i <= n; i++) { dp[i][i] = 0; } for (int len = 2; len <= n; len++) { for (int i = 1; i + len - 1 <= n; i++) { int j = i + len - 1; dp[i][j] = INF; for (int k = i; k < j; k++) { dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + getSum(i, j)); } } } cout << dp[1][n] << "\n"; return 0; }这道题最需要注意的是枚举长度len的外层循环。很多人第一次写会直接去枚举i和j,然后发现dp[i][k]和dp[k+1][j]还没算出来,结果全错。正确的做法是先枚举区间长度,再枚举起点,这样才能保证区间从小到大依次填充。prefix sum预处理getSum也是老生常谈,但真的很关键,没有它每次计算区间和都会多一个O(n)的复杂度,整体就变成O(n^4)了。
3. 机试实操现场的四个关键环节:语言、模板、对拍、复盘
刷题归刷题,但真正上考场之前的准备工作同样重要。语言选型、模板积累、对拍调试、复盘方法,这四个环节决定了你平时练的题能不能转化成分数。
3.1 编程语言怎么选:C++依然是机试的第一选择
除非学校明确限制编程语言,否则我强烈建议用C++。原因就三条:STL省时间、运行效率高、竞赛类题解资源最多。
很多人觉得Python写起来快,确实Python在实现复杂逻辑时代码更短,但机试题目对运行时间卡得紧的时候,Python的解法和C++完全不是一个量级。再加上很多机试环境里Python版本和第三方库不可控,用起来心里没底。Java也行,但代码量偏大,写起来手速吃亏。如果你基础还不牢,现在转C++完全来得及,Day9距离复试还有时间,每天用C++写题,两周就能把手感找回来。
语言定了之后,有一件事必须从今天开始养成习惯——IO加速。C++里写上这两行,能明显减少输入输出带来的超时风险:
ios::sync_with_stdio(false); cin.tie(nullptr);但要注意,如果混用了scanf/printf和cin/cout,开了sync_with_stdio(false)之后反而会出现读入顺序错乱的问题。所以定下来一套IO方式就别换来换去。
3.2 高频算法模板:不是背代码,是背"调试过的确定解"
考场上最大的心理负担是"我写的这个模板到底对不对"。平时练习时已经验证过无数次的模板,考场上直接调用,心态完全不一样。所以Day9晚上我花了专门的时间去整理模板,不是从网上抄,而是把自己写过、调过、AC过的代码提炼出来。
模板不是越多越好,而是要覆盖机试最高频的场景。我的模板清单是这样分类的:
- 数据结构类:并查集、单调栈、线段树、树状数组
- 图论类:Dijkstra堆优化、SPFA、Floyd、Kruskal、拓扑排序、二分图染色
- 算法技巧类:快速幂、前缀和差分、离散化、二分答案、滚动数组
- 字符串类:KMP、Trie树、字符串哈希
每一个模板都要能解释清楚关键部分为什么这么写。比如并查集路径压缩和按秩合并,这两种优化少一个都不是完整的模板:
class UnionFind { private: vector<int> parent, rank; public: UnionFind(int n) { parent.resize(n + 1); rank.resize(n + 1, 0); for (int i = 0; i <= n; i++) parent[i] = i; } int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } void unite(int x, int y) { x = find(x); y = find(y); if (x == y) return; if (rank[x] < rank[y]) { parent[x] = y; } else if (rank[x] > rank[y]) { parent[y] = x; } else { parent[y] = x; rank[x]++; } } bool same(int x, int y) { return find(x) == find(y); } };路径压缩的核心思想是在递归返回时把沿途的节点全部挂到根节点上,下次查找就是O(1)。按秩合并则是让矮树挂到高树上,避免退化成一条链。这两个优化一个都不能省,否则数据一大就会被卡超时。
3.3 对拍调试:比单点测试更靠谱的查错手段
平时刷题的时候,如果WA了,很多人的第一反应是肉眼debug,盯着一堆输出看半天。这个过程在Day9阶段太浪费时间了。我推荐你掌握"对拍"这个技巧。
所谓对拍,就是针对一个题目写一个暴力解法程序和一个高效解法程序,再用一个数据生成器随机产生输入,不断喂给两个程序,比较输出是否一致。如果发现不一致,说明高效解法和暴力解法的行为出现了分歧,这时候再针对那一组数据去调试,会非常高效。
数据生成器通常用随机数函数生成符合题目范围的数据。比如一个图的题目,生成器要保证生成的是合法图,有n个点m条边,边的权重在1到1000之间,且不能有自环和重边。这个过程本身也能让你加深对题目数据范围的理解。
3.4 复盘的正确姿势:把每道WA题变成你的私有题库
Day9晚上复盘的时候,我没有简单地对一下代码就结束。每一道没AC的题,我都会在笔记里记录三件事:
- 第一,这题考察的核心知识点是什么,我在哪个环节卡住了
- 第二,错误原因属于哪一类,是思路错了、边界漏了、还是语法写错了
- 第三,如果下次再遇到同类题,我的第一步反应应该是什么
比如说今天图论有一道Dijkstra的题,我一开始没用堆优化,直接写O(n^2)版本,结果在大数据上TLE了。复盘时我意识到,虽然n比较小的时候两种写法都行,但考场上的数据范围不会给你保证。习惯性的用法应该是一上来就写优先队列优化的版本。
4. 第九天刷题清单与真实踩坑记录
Day9这一天的晚间复盘里,我把当天遇到的真实问题全部记了下来。这些坑不是什么偏门冷知识,而是机试考场上最常见的翻车点。列成表放在这里,比光讲大道理有用得多。
| 题目/场景类型 | 踩到的坑 | 解决方式 | 优先级 |
|---|---|---|---|
| 区间DP石子合并 | 枚举区间时先写了i和j导致子问题未计算 | 先枚举长度len,再枚举起点i | 高 |
| Dijkstra堆优化 | 距离数组初始化为INT_MAX,加法溢出 | 用0x3f3f3f3f初始化INF | 高 |
| 并查集Kruskal | 忘了路径压缩,大数据TLE | find里递归压缩,unite按秩合并 | 中 |
| 字符串读入 | 用scanf读字符后残留换行符 | 统一用cin读string,或getchar消化 | 中 |
| DP数组开小 | 题目说n<=10000,二维数组每维10000导致MLE | 用滚动数组降维,或全局变量大数组 | 高 |
| 浮点比较 | 判断相等直接写==导致偏差 | 用eps=1e-8做绝对值比较 | 低 |
| 自定义排序 | 忘了重载operator<或cmp返回错误 | 统一用lambda,明确返回bool含义 | 中 |
4.1 一个图论综合题的完整AC复盘
今天图论专项里有一道"Kruskal+并查集求最小生成树"的基础题,虽然基础,但完整写一遍依然有价值。题目大意是给n个点m条带权无向边,求最小生成树的边权之和。
这道题要想AC,关键在两处。一是对所有边按照权重从小到大排序,二是依次尝试加入每条边,如果两个端点不在同一集合就合并,并累加权值。后者就是并查集的应用。
#include <bits/stdc++.h> using namespace std; struct Edge { int u, v, w; bool operator<(const Edge& other) const { return w < other.w; } }; vector<Edge> edges; vector<int> parent; int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; parent.resize(n + 1); for (int i = 1; i <= n; i++) parent[i] = i; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; edges.push_back({u, v, w}); } sort(edges.begin(), edges.end()); long long total = 0; int cnt = 0; for (auto& e : edges) { int fu = find(e.u); int fv = find(e.v); if (fu != fv) { parent[fu] = fv; total += e.w; cnt++; if (cnt == n - 1) break; } } if (cnt == n - 1) cout << total << "\n"; else cout << "impossible" << "\n"; return 0; }这道题我犯了一个很低级的错误:边数和点数输入反了,导致样例一直过不了。复盘时我给自己立了个规矩——每次写完输入输出逻辑,先花十秒钟检查变量名和题目描述的映射关系。这种错误在考场上特别容易出现,因为紧张,读题速度变快,字母一对应错就是白费半小时。
4.2 动态规划另一个高频变种:最长公共子序列的滚动数组优化
除了区间DP,今天还做了最长公共子序列的滚动数组版本。这个问题本身不复杂,状态转移方程是经典的:
if s1[i] == s2[j],则dp[i][j] = dp[i-1][j-1] + 1;否则dp[i][j] = max(dp[i-1][j], dp[i][j-1])。
但当字符串长度上到10000的时候,开一个10000乘10000的int二维数组直接就是400MB内存,必炸。所以可以用滚动数组,把dp数组压成两行,用cur和pre两个一维数组交替更新。
int lcs(const string& a, const string& b) { int n = a.size(), m = b.size(); vector<int> pre(m + 1, 0), cur(m + 1, 0); for (int i = 1; i <= n; i++) { for (int j = 1; j <= m; j++) { if (a[i - 1] == b[j - 1]) { cur[j] = pre[j - 1] + 1; } else { cur[j] = max(pre[j], cur[j - 1]); } } swap(pre, cur); } return pre[m]; }这个写法的核心就是利用"当前行只依赖上一行"这个特性。如果你平时不写滚动数组,等到考场上临时想,很可能在方向判断上出错。滚动数组的优化逻辑理解起来不难,难的是形成条件反射:凡是二维DP且数据范围超过5000,第一反应就应该是用滚动数组。
5. 第10天开始怎么走:从专题回归套题,补全复试全景
Day9做完,你应该有这样的感觉:单个题型的模板题已经不是障碍,真正的挑战在于考场上随机遇到各类题的综合处理和时间分配。所以第10天起,策略必须再调整一次。
5.1 Day10-13的专题收口建议
第10天到第13天,我计划把剩余的机试高频专题收口。每天上午还是408笔试复习,下午固定两小时机试训练。每天只做一个专题,贪心、搜索剪枝、树形DP、字符串处理,每个专题刷3到5题,重复做到不看题解能独立AC为止。
贪心的重点不是背结论,而是练"证明贪心正确性"的直觉。考场上你以为的贪心往往不是正确的贪心,只有通过反例测试才能筛选出真解法。搜索剪枝的重点则在于状态设计,BFS的visited数组记录什么、DFS的可行性剪枝怎么加,这都需要刻意练习。
5.2 第14天开始全天模拟赛
到了第14天,我会安排一次完整的模拟机试。找一套历年风格的题目,设定三小时倒计时,用真实的OJ环境或本地环境完成整套题目。这段时间里不允许查资料、不允许暂停,就是要逼自己在规定时间内完成读题、思考、编码、调试的全流程。
模拟赛最重要的收获不是做对了多少题,而是学会放弃。如果一个题目读题读了10分钟还没有思路,立即跳过做后面的题。确保所有会的题都拿到全分,比死磕一道难题差几分要划算得多。这个策略听上去简单,但很多人在考场上会大脑短路,明明能拿的分都丢了。
5.3 机试之外的准备也不能停:面试、项目与英语
从Day9开始,我每天会抽出睡前15分钟过一遍复试的其他材料。机试只是复试的一环,后面还有面试、项目问答和英语口试。每天15分钟不多,但能保证这些内容不会在机试结束之后才发现完全没准备。
面试环节老师最喜欢问的两类问题是"你做过什么项目"和"你在项目里解决过什么问题"。你需要准备一个简历上的项目,梳理它的核心模块、技术栈和你在里面扮演的角色。项目的复杂度不要求多高,但一定要能够把自己的想法完整表达出来。而英语口试部分,准备一个常见的自我介绍,再针对"为什么选择这个方向"、"研究生阶段的规划"这类问题准备一小段回答,就基本够用了。
结尾
Day9这一整天的复盘写到这里,我把今天最重要的体会浓缩成一句话:机试比的是稳定输出,不是灵光一现。到了这个阶段,你不需要再追求一天刷几十道题的快感,而是要把每一道错题的原因彻底搞懂,把每一个模板调试到无懈可击,把每一类题型形成固定的解题流程。
最后再分享一个小技巧:从Day9开始,每次提交代码之前,我都强迫自己先口述一遍"我这段代码在最坏情况下会不会超时、会不会爆内存、会不会溢出"。这十秒钟的自我问答,已经帮我拦下了至少三次低级错误,效果比编译通过后再调试快得多。希望这篇Day9的复盘对你有用,也祝每一个正在准备机试的人都能在考场上遇到自己练过的题。