2025年川大计算机考研复试的机试刚结束那几天,我几乎天天泡在考生群里翻题目回忆。今年题型的整体脉络比往年清晰,但也确实有几个细节让不少人在考场上卡了壳。这篇整理就是我结合考生回忆和历年机试风格做的一次完整复盘:每道题的解题思路、AC代码、考场上容易翻车的点,以及我实际调试时踩过的坑。正在准备川大计算机相关专业复试的同学可以直接拿去当备考目录,其他在刷OJ的人也能从中找到一些通用技巧。
先说明一下背景:川大机试通常用学校自建OJ,编译器以g++为主,也有人用Java写,测评时Linux环境,输入输出严格比对。题量一般五道左右,难度呈阶梯式,第一题往往是送分题,到最后会有一道带一点算法含量的题卡住不少人。下面这些题目是我按考察方向重新梳理的还原版本,细节上做了必要的补全,但考点的分布、常见陷阱和判题逻辑都是实测过才写下来的。
1. 复试机试的整体风格与备考思路
1.1 机试到底在考什么
今年川大的机试题目范围依然很“基础”:字符串处理、简单模拟、二叉树遍历、并查集、以及一些最基础的动态规划。这个范围看起来不大,却非常考验临场写代码的准确性。很多同学在牛客和力扣上刷惯了“算法题”,到了OJ上测试点一加多就崩,问题往往出在输入读取、边界条件、输出格式这些地方。
我复盘近几年题目后,发现一个明显的趋势:题目背景越来越“情景化”,但内核还是经典题型。比如字符串题会包装成“清理日志中的敏感词”,图论题会包装成“校园网络连通检测”,本质上不过是逆序输出单词、并查集这种基础操作。这个特点对备考来说其实是好事,你不需要追求刷特别偏的怪题,把常见数据结构和经典题型吃透就能覆盖九成考点。
另一个容易被忽视的点是时间分配。考场两个小时,五道题,第一题大概10分钟要拿下,中间三道每题控制在20-30分钟,最后一道难题留40分钟左右。我见过太多人第一题想太多,写了三种解法还在怀疑自己,最后导致后面时间不够。
1.2 备考方向的三个关键调整
如果你现在还在用刷力扣的方式准备复试机试,我建议做三个调整。
第一,从“刷难题”转向“写对题”。机试考察的并不是你知不知道最优解,而是能不能在限时环境里写出一个完全正确的版本。听说过思路但写不出AC代码,在机试里等于没有思路。所以备考必须用OJ实际提交,而不是只在IDE里跑两遍觉得“差不多”就算过。
第二,输入输出习惯要提前养成。很多自建OJ的输入样例不会给你太多提示,它可能包含多组数据、空行、行末多余空格、数字之间多个空格。这些细节在力扣的模板里根本遇不到。我建议备考期间所有练习都走标准输入输出,而不是依赖封装好的Solution函数。
第三,善用调试输出,但提交前一定关掉。考场调试时printf大法是正常的,问题是很多人最后忘了把调试代码注释掉,交上去直接输出一堆垃圾。这属于完全可以通过习惯避免的失分。
2. 真题类型拆解与解题思路
2.1 字符串处理:句子单词顺序反转
先看一个典型的签到题,今年考到类似方向的题目不在少数。题面大致是:输入若干行英文句子,每行包含若干单词,要求将这一行中的单词顺序反转后输出。例如输入 "I love Sichuan University",输出 "University Sichuan love I"。句子中可能有多个空格,需要忽略多余空格。
这种题在C++里最稳妥的写法是用 stringstream 做分词,避免手动遍历空格时出各种边界问题。核心思路:读入整行,然后用 stringstream 将单词逐个提取到 vector 中,最后逆序输出。下面是能完整AC的版本:
#include <bits/stdc++.h> using namespace std; int main() { string line; while (getline(cin, line)) { if (line.empty()) continue; stringstream ss(line); vector<string> words; string w; while (ss >> w) { words.push_back(w); } reverse(words.begin(), words.end()); string res; for (int i = 0; i < words.size(); i++) { if (i > 0) res += ' '; res += words[i]; } cout << res << endl; } return 0; }这段代码里最值得留意的是我单独拼了一个 res 字符串,而不是直接在循环里打印“单词加空格”。这是一个非常容易踩坑的地方:如果直接 cout << words[i] << " ",最后一个单词后面会多一个空格,OJ的判题器通常会判定格式错误。用 res 拼接时跳过最后一个空格,就彻底规避了这个问题。
还有一个坑是输入第一行可能有一个多余换行或空行。上面代码里的if (line.empty()) continue;就是用来兜底的。虽然大多OJ不会在真实数据里放空行,但多写这一行不亏,程序更健壮。
如果考场选Java,思路完全一样,只是读取方式要改一下。用 BufferedReader 的 readLine 读整行,再用line.trim().split("\\s+")分词。这里注意 split 的参数必须写\\s+而不是一个空格,因为题目说了可能有多个连续空格。
2.2 二叉树:递归建树与层序遍历最大值
第二道常考的是二叉树相关,今年有个题目虽然包装成了“按层统计信号强度最大值”,但本质就是二叉树的层次遍历。题面给出一棵二叉树的先序遍历和中序遍历序列,要求重建这棵树,并输出每一层节点值的最大值。
第一步是建树。先序遍历的第一个节点一定是根,在中序遍历里找到根的位置,就能自然分出左子树和右子树的范围。这个递归过程网上有很多版本,但很多人写错位置或者忘记处理区间过界。我给出一个实测没问题的写法:
#include <bits/stdc++.h> using namespace std; struct TreeNode { int val; TreeNode* left; TreeNode* right; TreeNode(int x) : val(x), left(nullptr), right(nullptr) {} }; TreeNode* buildTree(vector<int>& pre, int pl, int pr, vector<int>& in, int il, int ir) { if (pl > pr || il > ir) return nullptr; int rootVal = pre[pl]; TreeNode* root = new TreeNode(rootVal); int pos = il; while (in[pos] != rootVal) pos++; int leftLen = pos - il; root->left = buildTree(pre, pl + 1, pl + leftLen, in, il, pos - 1); root->right = buildTree(pre, pl + leftLen + 1, pr, in, pos + 1, ir); return root; }建树之后用队列做层序遍历,并记录当前层的节点数量。这个“按层记录size”是层次遍历里最常见也最容易理解的方法。设层大小为 n,那么本层从队列里弹出 n 个节点,下一层的节点自然全部入队,刚好能凑成新的 n。循环往复,直到队列为空。
vector<int> levelMax(TreeNode* root) { vector<int> ans; if (!root) return ans; queue<TreeNode*> q; q.push(root); while (!q.empty()) { int sz = q.size(); int curMax = INT_MIN; for (int i = 0; i < sz; i++) { TreeNode* cur = q.front(); q.pop(); curMax = max(curMax, cur->val); if (cur->left) q.push(cur->left); if (cur->right) q.push(cur->right); } ans.push_back(curMax); } return ans; }这个知识点在复试里太常出现了,几乎可以当成必考内容准备。备考时我建议要把递归建树的几种变体都写一遍,尤其是给出层序序列或者后序+中序时该怎么处理。别以为背一版就万事大吉,考场换一个序列组合,很多人的递归边界就写不对了。
2.3 图论:并查集与连通块数量
再来看一道带一点点算法味道的题目:校园网络里有 n 个节点、m 条边,给定 q 个查询,每次问两个节点是否连通,最后还要输出整个网络有多少个连通块。考察点很直接,就是并查集。
为什么选并查集而不是BFS或DFS?因为这道题只问“两个点是否连通”和“连通块数量”,不需要求最短路径。如果每次查询都跑一遍DFS,数据量一大必然超时。并查集把查询做到近似 O(1),整体复杂度轻松可控。
下面是标准实现,我特意把初始化、路径压缩和合并都写全,方便直接套用:
#include <bits/stdc++.h> using namespace std; const int MAXN = 1005; int fa[MAXN]; int find(int x) { if (fa[x] == x) return x; return fa[x] = find(fa[x]); // 路径压缩 } void merge(int a, int b) { int ra = find(a); int rb = find(b); if (ra != rb) { fa[ra] = rb; } } int main() { int n, m, q; scanf("%d%d%d", &n, &m, &q); for (int i = 1; i <= n; i++) fa[i] = i; for (int i = 0; i < m; i++) { int a, b; scanf("%d%d", &a, &b); merge(a, b); } while (q--) { int u, v; scanf("%d%d", &u, &v); if (find(u) == find(v)) printf("YES\n"); else printf("NO\n"); } int blocks = 0; for (int i = 1; i <= n; i++) { if (find(i) == i) blocks++; } printf("%d\n", blocks); return 0; }这里有个细节容易出错:最后统计连通块数量时,不能直接看fa[i] == i,必须先执行一次 find 路径压缩,因为之前的路径压缩可能并没有覆盖所有节点。虽然一般数据规模下直接判断也能过,但严谨起见,统计前再跑一次 find 是最稳的。
还有一点,如果题目给的是多组测试数据,每组 n、m、q 都要重新初始化 fa 数组。我见过不少人在单组数据的题目里不初始化,换到多组数据就直接崩。
3. 核心实现细节与耗时坑点
3.1 每道题背后的复杂度和考点分析
把上面的题整理成一个表格,备考时可以直接对照着查漏补缺:
| 题目方向 | 考点 | 核心复杂度 | 易错点 |
|---|---|---|---|
| 单词逆序 | 字符串分词、STL容器 | O(n) | 多空格和输出末尾空格 |
| 二叉树层序最大值 | 递归建树、BFS | O(n) | 递归边界、空树判断 |
| 并查集连通性 | 路径压缩、连通块统计 | 近似 O(alpha n) | 初始化、重复路径压缩 |
如果你的目标只是“稳过机试”,这三类题目必须做到看到题面就能动手,不需要思考太久。字符串、二叉树、图论基础数据结构,在历年题目中出现的频率极高,练熟它们比贪多刷难题性价比高得多。
考察难度方面,第一题基本就是送分,第二题会有一点代码量,第三题开始真正区分“会写”和“写得对”。所以机试的核心竞争力,其实在于“写得对”。
3.2 输入输出优化的实际必要性
很多同学在自建OJ上第一次提交时会遇到这样的情况:本地跑样例秒出结果,一交上去就是 Time Limit Exceeded。原因很多时候不是算法复杂度太高,而是输入输出方式太慢。C++里cin/cout默认关联标准C的输入输出,每次读写都会同步刷新缓冲区,在大数据量下非常吃亏。
解决方式很简单。在 main 函数最开始加两行:
ios::sync_with_stdio(false); cin.tie(0);如果还担心,直接用scanf/printf是最稳的选择。上面并查集那题我就特意用了scanf/printf,因为它涉及多组大输入。而二叉树那题的输入量不大,用cin也没有问题。这个选择要灵活,不要死记一种风格。
Java选手也要注意:Scanner在大数据量下性能很差,复试机试用Java的话,强烈建议用BufferedReader + StringTokenizer来读。虽然写起来别扭,但可以避免很多“明明算法对,却超时”的情况。
3.3 考场上如何快速确认自己写对了
我自己的习惯是,每一题AC之后绝不马上交。停下来做三个小测试:第一,用题目给的样例跑一遍;第二,自己造一个最小边界数据(比如 n=1、空树、只有两个节点)跑一遍;第三,造一个最大规模的数据看时间是否可接受。这三个测试都通过再提交,成功率会高很多。尤其在机试这种“一题定胜负”的场景里,稳比快重要。
4. 常见问题与调试技巧实录
4.1 现场最容易翻车的五件事
每次机试结束,群里都会涌现出各种“莫名其妙”的WA报告。我梳理了这几年帮忙复盘时最常看到的几个问题,按出现频率排序:
第一,输出格式错。大多是行尾多了一个空格,或者该换行的地方没有换行。题目如果明确说“每个样例输出占一行”,那必须严格遵守。拼字符串时最好用临时变量统一处理,不要在循环里直接打印“数据加空格”,这是我反复强调的一点。
第二,数组越界或者递归死循环。二叉树建树时的递归参数最容易写错。我曾经在一次模拟中把buildTree(pre, pl+1, pl+leftLen, in, il, pos-1)写成了pl+leftLen-1,导致左子树区间被吞掉,整个程序直接越界。排查这种问题只能靠递归边界反复推演,没有捷径。
第三,空指针或Java空引用。C++里访问了 NULL 节点的成员,运行直接异常;Java里对象为 null 再调用方法,直接报空指针。解决方法是所有涉及到root->left、root->right的地方,先判空再操作。
第四,死循环。while(cin >> n)这种写法在正式OJ里经常会因为提前遇到 EOF 而退出,但在某些场景下如果循环体内没有正确读取,或者读取了不该读的东西,就会卡死。建议用getline读字符串时特别注意行尾换行符的清理。
第五,没有初始化。并查集的 fa 数组、邻接表的表头、DP数组的初始值,这三样忘了初始化,输出就会变成一堆随机数。检查顺序放在写代码最后一步,每次提交前看一眼初始化逻辑。
4.2 用“对拍”快速定位逻辑错误
如果你在练习时遇到“样例过了但提交WA”的情况,我特别推荐一种调试方法:对拍。思路很简单,写一个保证正确但可能很慢的暴力程序,题目用到的数据结构无关紧要,只要结果对就行。然后写一个生成小规模随机数据的脚本,让暴力程序和正式程序都读同一份数据,比对输出,不一致就能定位到具体输入。
我用C++刷OJ时的对拍流程大致是:写两个程序分别编译为force.exe和solve.exe,再用一个简单的 Python 脚本生成数据,轮流跑两个.exe比对输出。比如针对字符串反转题,暴力程序就老老实实遍历,正式程序用 stringstream。随机测1000组数据,如果全一致,基本可以放心提交。
这个习惯看起来麻烦,但对提升AC率非常有效。特别是二叉树、并查集这类边界条件多的题,手算样例很难覆盖所有情况,对拍能快速找出隐藏错误。
4.3 提交前五分钟检查清单
机械地按顺序过一遍,能拦住大部分低级错误:
- fa数组或DP数组有没有初始化?
- 递归终止条件是否覆盖了空节点和空区间?
- 输出是否有多余空格?行尾是否多了空格?
- 是否用了
ios::sync_with_stdio(false)?用cin/cout的话这行必不可少。 - 题目要求读多组数据,但我是不是只读了一组?
- 有没有遗留的调试输出?
这六条我在自己动手写代码时都会作为纪律执行。机试环境里没有IDE的智能提示,也没有人帮你检查,唯一的防线就是自己的提交习惯。
5. 备考路线与考场策略建议
5.1 剩余一个月怎么安排机试刷题
从初试结束到复试通常有一个多月的时间。这个阶段我不建议再去啃新的算法专题,优先级应该是:基础语法熟练度优先于所谓的高级算法。具体安排可以参考下面这个节奏:
第一周,集中过一遍字符串处理和模拟题,每天保证写三道完整题目,重点练getline、stringstream、scanf的输入切断。第二周,复习基础数据结构,栈、队列、二叉树遍历、并查集,各找十道左右经典题写熟。第三周,练一点简单DP,最长上升子序列、0-1背包、斐波那契类题目足够。第四周,找自建OJ或者同类OJ做模考,严格按照考试时间两个小时完成一套题目,训练自己分配时间的感觉。
顺便说一句,不要为了追求“高级算法”去啃网络流、后缀数组这类知识点。复试机试基本不会考到,把精力花在性价比低的地方等于浪费宝贵的备考时间。
5.2 考场心态与时间分配
机试现场最容易出现的心理问题是:第一题迟迟不敢动手写。看到题面越想越复杂,总觉得自己漏掉了什么高端解法。我的经验是,机试大多数题都是“看着吓人”,实际往往只需要最直白的方法。签到题就应该用最短时间拿下,给自己一个正反馈。
如果一道题卡了二十分钟还没有思路,果断跳过,先做后面的题。机试是按总题数计分的,一道难题的性价比远不如两道简单题。把能拿的分全部拿完,剩余时间再回头啃难题,这是最稳妥的策略。
还有一件事必须提醒:提交前一定仔细看题目给定的“输入输出样例”是否与代码逻辑一致。很多WA不是因为思路错,而是index从1开始还是从0开始的问题。图论题特别容易在这里踩坑,节点的编号范围一定要先确认清楚。
最后想单独聊聊考场之外的准备。如果你能在复试前完整跑通三次模拟测试,适应连续两小时的码代码节奏,那你在真实考场上崩掉概率就很小。很多同学平时刷题一次只写一道,突然要求两小时写五道,往往不是不会写,而是写着写着就疲了,后面几题草草了事。机试虽然名义上考算法,实际上还考专注力和手速,这个只能靠限时训练来补。
我自己复盘时体会最深的一点是:机试题目并没有太多“超纲”内容,真正的区分度都在基础细节里。字符串处理时的空格处理、递归建树时区间的边界判断、并查集的初始化,这些看起来微不足道的地方,恰恰是决定AC还是WA的分水岭。如果你现在还在为复试慌,不妨把这几道题的代码自己动手重写一遍,比看十篇经验帖都管用。