☰
哈工大计算机复试机试真题详解:高频算法题解与AC模板
2026/9/30 11:46:29 网站建设 项目流程

2025年哈工大计算机复试机试刚落下帷幕,考研群里已经陆续有人贴出回忆版题目。我把出现频率高、代表性强的几道题整理成了这篇带完整解法的文章,从出题意图、推导思路到可以直接跑的AC代码都拆开讲。如果你正在准备哈工大或者同类学校的复试机试,这篇文章完全可以当作考前模板库用。机试这东西,说穿了考的不是智商,而是你在绝对紧张的状态下,能不能把最基础的算法稳定地写出来、调通、提交通过。

1. 先别急着刷题:把哈工大机试这件事看透了

1.1 机试在复试中的定位:它考的是“稳定输出”而不是“炫技”

很多人准备机试有一个误区,以为要学一堆竞赛级别的奇技淫巧,什么网络流、后缀自动机、平衡树,拼命往深处钻。实际上哈工大机试的难度定位非常明确:考察基本的编程实现能力和算法基础,不追求偏难怪。从历年题目看,核心范围基本锁死在模拟、链表、栈与队列、二叉树、简单图论、线性DP、并查集、字符串处理这些主题上。

机试在复试总分中的占比不低,具体比例每年以复试细则为准,但有一点是确定的:机试不过关,笔试面试发挥得再好也白搭。我见过不少初试高分选手,算法原理说起来头头是道,一上OJ就卡在编译错误和边界条件上,最后遗憾落榜。这个环节刷掉的人,不是不会算法,而是写不完整、调不出来、时间分配崩溃。所以准备机试的核心策略,就是要把高频题型练到肌肉记忆,做到看到题直接浮现模板,而不是现场推演。

1.2 考试环境与评测规则:这些信息比刷题本身更重要

哈工大机试通常使用黑盒评测,也就是ACM模式,程序从标准输入读数据,往标准输出写结果,评测机拿到你的输出和标准答案比对。这意味着三个很现实的约束:

第一,你必须习惯自己处理输入输出格式,任何多余的空格、换行、提示语都是错。第二,多组输入是很常见的设定,代码要考虑EOF结束、每组数据之间是否需要清空状态。第三,C++是全场最稳妥的选择,STL容器和算法库能极大减少手写数据结构的时间和出错概率。虽然部分年份允许Java或Python,但考场时间有限,C++配合bits/stdc++.h这道万能头文件,基本能覆盖绝大多数情况。

我建议备考时直接上洛谷、牛客这种标准OJ练习,不要只在IDE里写完看一眼输出就完事,必须强迫自己提交、看评测结果,因为WA和RE的反馈体验在考场上是一模一样的。

2. 2025年考生回忆版真题拆解:六道题从题意到AC

每一道题我都按照“题意还原、考察意图、思路推导、AC代码、注意事项”五个层次来讲。题目描述来自考后回忆,细节可能有出入,但考察点是完全真实的。

2.1 双向链表的区间翻转:最容易被“指针绕晕”的模拟题

题意还原:给定一个长度为n的双向链表,每个节点的值为1到n,要求翻转从位置l到位置r这一段节点,然后按链表顺序输出所有节点的值。

考察意图:这道题看着简单,实际是在考两个东西:第一,你熟不熟悉双向链表的前驱和后继关系;第二,你的边界处理能力。如果真去用new Node()一个个动态建节点再翻转指针,大概率会写乱。机试里最好的做法是用数组模拟链表,用一个pre[]数组存前驱下标,nxt[]数组存后继下标,这样调试起来一目了然,也不用担心内存泄漏。

思路推导:核心是三步走。第一步找到区间左端点的前驱L和区间右端点的后继R,把整个区间从链表上摘下来;第二步遍历区间内每个节点,把它的前驱指针和后继指针交换,实现区间内部反向;第三步把翻转后的子链表重新接回L和R中间。

这里有一个关键设计:在链表的头和尾分别加一个哨兵节点0和n+1。加了哨兵,l等于1和r等于n的情况就不用特判了,L和R永远都有有效值。

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int pre[MAXN], nxt[MAXN]; int n, l, r; int main() { while (scanf("%d%d%d", &n, &l, &r) != EOF) { // 0 为头哨兵,n+1 为尾哨兵,所有节点初始化为正常双向链表 for (int i = 1; i <= n; i++) { pre[i] = i - 1; nxt[i] = i + 1; } pre[1] = 0; nxt[n] = n + 1; int L = l - 1; // 区间左侧的前驱 int R = r + 1; // 区间右侧的后继 // 第一步:把区间 [l, r] 从原链表中断开 int subHead = l; int subTail = r; nxt[L] = R; pre[R] = L; // 第二步:翻转区间内部的 pre 和 nxt 指针 int cur = subHead; while (cur != R) { int originalNext = nxt[cur]; // 必须先把原后继存下来 swap(pre[cur], nxt[cur]); cur = originalNext; } // 第三步:重新接回,翻转后区间的头变成 subTail,尾变成 subHead nxt[L] = subTail; pre[subTail] = L; nxt[subHead] = R; pre[R] = subHead; // 从头哨兵开始输出 for (int i = nxt[0]; i != n + 1; i = nxt[i]) { if (i != nxt[0]) printf(" "); printf("%d", i); } printf("\n"); } return 0; }

注意事项:翻转内部指针时,最容易犯的错误就是遍历方向搞混。因为你在循环里交换了指针,如果仍用cur = nxt[cur]去移动,一旦走到当前节点,nxt[cur]已经变成原来的前驱,就会往回走,造成死循环或重复翻转。解决方案就是在交换之前先把原后继暂存下来,我代码里的originalNext变量就是干这个的。另外,本题如果要求输出节点的值而不是编号,只需要把i换成val[i]即可,思路完全一样。

2.2 二叉树的重建与层序遍历:一旦掌握就是白给题

题意还原:给出某二叉树的前序遍历序列和中序遍历序列,节点值互不相同,要求输出该二叉树的层序遍历序列。

考察意图:二叉树遍历是数据结构课的必修内容,机试里出现毫不意外。这道题真正考的是递归划分的思维:前序遍历第一个节点一定是根,拿着根去中序序列里定位,左边是左子树、右边是右子树,然后递归处理。

思路推导:先用哈希表把中序序列每个值对应的下标存下来,这样每次找根的位置就是O(1)时间,整体复杂度O(n)。递归建树的过程就是不断缩小前序和中序的区间,左子树的长度由中序列中根的位置决定,这步理解了,整道题畅通无阻。建完树以后用标准BFS输出即可。

#include <bits/stdc++.h> using namespace std; struct Node { int val; Node *left, *right; Node(int v) : val(v), left(nullptr), right(nullptr) {} }; vector<int> pre, in; unordered_map<int, int> pos; Node* build(int preL, int preR, int inL, int inR) { if (preL > preR) return nullptr; int rootVal = pre[preL]; Node* root = new Node(rootVal); int idx = pos[rootVal]; int leftLen = idx - inL; root->left = build(preL + 1, preL + leftLen, inL, idx - 1); root->right = build(preL + leftLen + 1, preR, idx + 1, inR); return root; } int main() { int n; while (cin >> n) { pre.resize(n); in.resize(n); pos.clear(); for (int i = 0; i < n; i++) cin >> pre[i]; for (int i = 0; i < n; i++) { cin >> in[i]; pos[in[i]] = i; } Node* root = build(0, n - 1, 0, n - 1); queue<Node*> q; q.push(root); bool first = true; while (!q.empty()) { Node* curNode = q.front(); q.pop(); if (!first) cout << " "; first = false; cout << curNode->val; if (curNode->left) q.push(curNode->left); if (curNode->right) q.push(curNode->right); } cout << endl; } return 0; }

注意事项:这道题有个细节,多组输入时unordered_map一定记得clear(),否则残留数据会直接导致下标定位错误。层序遍历输出的节点间空格格式也要严格按题目要求,很多同学WA不是算法错,而是输出格式多了一个行尾空格。如果题目要求输出后序遍历,只需要在递归返回前打印根节点的值,其他部分完全不变,这类变形建议考前都练一遍。

2.3 带路径输出的最短路:Dijkstra模板别只背一半

题意还原:给定n个点m条带权无向边,求从节点1到节点n的最短路径长度,并输出一条最短路径经过的节点序列。

考察意图:最短路是机试图论题的重头戏,但大部分同学背模板只背到dist[]数组,忽略了路径记录。这道题就是要考察你能否在标准的堆优化Dijkstra里顺手维护前驱节点。

思路推导:堆优化Dijkstra用优先队列维护当前距离最小的点,核心松弛条件是若dist[u] + w < dist[v],则更新dist[v]并记录pre[v] = u。路径输出用一个小技巧:从终点n不断沿着pre[]回溯到起点,再翻转vector得到正向路径。这里优先队列里存的是{距离, 节点编号},并且用greater实现小顶堆。

#include <bits/stdc++.h> using namespace std; const int INF = 0x3f3f3f3f; const int MAXN = 10005; struct Edge { int to, w; }; vector<Edge> graph[MAXN]; int dist[MAXN], pre[MAXN]; int n, m; void dijkstra(int s) { memset(dist, INF, sizeof(dist)); memset(pre, -1, sizeof(pre)); dist[s] = 0; priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq; pq.push({0, s}); while (!pq.empty()) { auto [d, u] = pq.top(); pq.pop(); if (d != dist[u]) continue; // 过期节点直接跳过 for (auto& e : graph[u]) { int v = e.to; if (dist[v] > dist[u] + e.w) { dist[v] = dist[u] + e.w; pre[v] = u; pq.push({dist[v], v}); } } } } int main() { while (cin >> n >> m) { for (int i = 1; i <= n; i++) graph[i].clear(); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; graph[u].push_back({v, w}); graph[v].push_back({u, w}); } dijkstra(1); if (dist[n] == INF) { cout << "No path" << endl; continue; } vector<int> path; for (int cur = n; cur != -1; cur = pre[cur]) { path.push_back(cur); } reverse(path.begin(), path.end()); cout << dist[n] << endl; for (int i = 0; i < path.size(); i++) { if (i) cout << " "; cout << path[i]; } cout << endl; } return 0; }

注意事项:这道题有两个容易翻车的点。第一,if (d != dist[u]) continue;这行必须有,它的作用是跳过已经被更新过的旧记录,没有它复杂度会退化;但注意这里用!=而不是>,因为在非负权图中,堆顶记录要么等于最新最短路,要么是过期值,用>反而可能错过有效更新。第二,路径回溯前pre数组要先全部初始化为-1,否则如果目标节点不可达,回溯循环会越界。另外,如果题目要求字典序最小的路径,可以在相等距离时比较pre的字典序,或者对邻接表按节点编号排序后用严格小于更新。

2.4 最长递增子序列:两种复杂度都要烂熟于心

题意还原:给定一个长度为n的整数序列,求最长严格递增子序列的长度。

考察意图:线性DP是机试最爱考的DP类型,而LIS是其中最经典的载体。这道题下限可以O(n^2)暴力DP,上限可以O(nlogn)贪心二分,很多考生只记得nlogn的lower_bound写法,却推导不出原理,一换条件就懵。

思路推导:O(nlogn)的核心思路是维护一个数组d[],d[len]表示长度为len的递增子序列的最小末尾值。遍历原序列时,用二分找到第一个大于等于a[i]的位置pos,把a[i]放到那里去,如果pos大于当前len,说明这个数接上了更长的子序列,len加一。严格递增用lower_bound,如果题目改成了非递减,就用upper_bound,这个区别值得你标记在笔记旁边。

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int a[MAXN], d[MAXN]; int main() { int n; while (cin >> n) { for (int i = 1; i <= n; i++) cin >> a[i]; int len = 0; for (int i = 1; i <= n; i++) { int pos = lower_bound(d + 1, d + len + 1, a[i]) - d; d[pos] = a[i]; if (pos > len) len = pos; } cout << len << endl; } return 0; }

注意事项:这套O(nlogn)模板虽然简短,但只适用于求长度。如果题目要求输出具体的最长递增子序列,d[]数组本身并不是最终序列,必须额外用pre[]数组记录每个数在DP过程中的前驱下标,然后从最后一个数回溯。我备考时踩过这个坑:现场临时加路径输出把自己绕晕了,最后AC代码还是回到O(n^2)带路径版本。另外注意如果用while (cin >> n)处理多组输入,d[]数组不需要清空,因为每次会用len重新限定范围,但a[]数组要重新读入。

2.5 中缀表达式求值:考场上的“纸老虎”

题意还原:给定一个由数字、+、-、*、/和括号组成的中缀表达式,计算其值,除法为整除。

考察意图:表达式求值几乎是每年机试的常驻题型。它不涉及高深算法,但极其考验代码的细致程度,运算符优先级、括号匹配、数字的连续读取,任何一处疏忽都会WA。

思路推导:双栈法是标准解法:一个栈存操作数,一个栈存运算符。遍历字符串时,遇到数字就完整读取一串数字入栈;遇到左括号直接压栈;遇到右括号就一直计算栈顶,直到遇到左括号;遇到运算符,先处理掉栈顶所有优先级不低于当前运算符的运算符,再压栈。遍历结束后把栈里剩余的运算全部执行完,操作数栈顶就是答案。

#include <bits/stdc++.h> using namespace std; int applyOp(int a, int b, char op) { if (op == '+') return a + b; if (op == '-') return a - b; if (op == '*') return a * b; return a / b; } int main() { string s; while (getline(cin, s)) { stack<int> nums; stack<char> ops; int priority[256] = {}; priority['+'] = priority['-'] = 1; priority['*'] = priority['/'] = 2; for (int i = 0; i < (int)s.size(); i++) { if (s[i] == ' ') continue; if (isdigit(s[i])) { int num = 0; while (i < (int)s.size() && isdigit(s[i])) { num = num * 10 + (s[i] - '0'); i++; } i--; nums.push(num); continue; } if (s[i] == '(') { ops.push(s[i]); continue; } if (s[i] == ')') { while (!ops.empty() && ops.top() != '(') { int b = nums.top(); nums.pop(); int a = nums.top(); nums.pop(); char op = ops.top(); ops.pop(); nums.push(applyOp(a, b, op)); } ops.pop(); // 弹出左括号 continue; } // 处理运算符,包括负数开头的特殊情况 if (s[i] == '-' && (i == 0 || s[i - 1] == '(')) { nums.push(0); } while (!ops.empty() && priority[ops.top()] >= priority[s[i]]) { int b = nums.top(); nums.pop(); int a = nums.top(); nums.pop(); char op = ops.top(); ops.pop(); nums.push(applyOp(a, b, op)); } ops.push(s[i]); } while (!ops.empty()) { int b = nums.top(); nums.pop(); int a = nums.top(); nums.pop(); char op = ops.top(); ops.pop(); nums.push(applyOp(a, b, op)); } cout << nums.top() << endl; } return 0; }

注意事项:表达式求值有一个大家都不太在意的坑:负号。比如表达式-3+5,遍历到-的时候,操作数栈是空的,按常规写法会再把栈顶两个数弹出来计算,直接就崩了。我的解决方案是在-号前面补一个数字0,把它当成0 - 3 + 5处理。判断条件是当前字符是-并且它位于表达式开头或者前面紧跟着左括号。同样地,2*(-3)这种带括号的负号也要用这个办法处理。另外,空格的处理、多位数读取、除0问题(题目一般不会给,但保险起见可以用整除)都要在交卷前过一遍。

2.6 带权并查集:近几年越来越爱考的一类

题意还原:有n个元素,初始各自为集合。m条操作,第一种是查询x与y的关系,能确定则输出关系,不能确定则输出未知;第二种是给定x、y以及它们之间的差值w,表示y比x大w,进行合并。

考察意图:普通并查集大家都背过,但带权并查集是很多人的盲区。哈工大近年明显加大了对这类题的考察力度,因为它既考并查集结构,又考数学推导能力,难度区分度很好。

思路推导:核心是维护两个数组:fa[x]表示x的父亲,val[x]表示x到fa[x]的权值,或者说“差值”。find函数做路径压缩时,递归找到根节点后,要把val[x]累加上val[fa[x]],这样压缩后val[x]就直接表示x到根节点的差值。合并时,如果x的根是rx,y的根是ry,需要把ry挂到rx下面,并计算val[ry]使得val[y] - val[x] = w成立。推导结果就是val[ry] = w + val[x] - val[y]。

#include <bits/stdc++.h> using namespace std; const int MAXN = 100005; int fa[MAXN], val[MAXN]; // val[x] 表示 x 到 fa[x] 的差值,即 x - fa[x] int find(int x) { if (fa[x] == x) return x; int root = find(fa[x]); val[x] += val[fa[x]]; // 路径压缩时累加差值 return fa[x] = root; } int main() { int n, m; while (cin >> n >> m) { for (int i = 1; i <= n; i++) { fa[i] = i; val[i] = 0; } while (m--) { int type, x, y; cin >> type >> x >> y; if (type == 1) { // 查询 int rx = find(x), ry = find(y); if (rx != ry) { cout << "Unknown" << endl; } else { cout << val[y] - val[x] << endl; // y - x 的差值 } } else { // 合并,输入 w 表示 y - x = w int w; cin >> w; int rx = find(x), ry = find(y); if (rx != ry) { fa[ry] = rx; val[ry] = w + val[x] - val[y]; } } } } return 0; }

注意事项:合并公式val[ry] = w + val[x] - val[y]推导的时候很多人懵,我给你拆解一下。路径压缩后,val[x]是x到rx的差值,val[y]是y到ry的差值。我们想让ry以rx为父,也就是要满足val[y] + val[ry] - val[x] = w,整理一下就是val[ry] = w + val[x] - val[y]。这个推导要当作模板的一部分背下来,考场上现场推太费时间。另外,find路径压缩必须用递归写法,否则更新val[x]的时机不对;如果你对爆栈有顾虑,可以改循环写法但难度翻倍,不建议考场尝试。

3. 考场实战:从读题到AC的完整工作流

3.1 三分钟读题法:先看数据范围,再想算法

机试和平时刷题最大的区别就是时间压力,一道题不可能留出半小时慢慢推。我自己的习惯是拿到题先不急着读故事背景,直接扫三样东西:n和m的取值范围、输入输出格式、有没有特殊修饰词。数据范围决定算法,这是最朴素也最有效的判断依据:n小于1000,O(n^2)随便写;n到1e5,必须上O(nlogn);n到1e9,基本是数学公式题。输出格式里有“字典序”“不超过”“严格”这类词,都是出题人埋的坑,必须圈出来。

读题三分钟以内必须做出判断并开始写代码,切忌反复怀疑自己是不是漏了什么条件。机试题目不会像竞赛题那样藏着弯弯绕绕的深意,大部分是直来直去的,宁可写完后反复对样例,也不要迟迟不动手。

3.2 先搭框架:输入、输出、主流程按固定顺序写

很多新手一上来就写核心算法,写到一半发现输入漏读了一个变量,又回去改,代码结构一团糟。我的习惯是任何题目都先写三段框架:先是while(scanf(...) != EOF)的读入循环,把题目要求的输入格式全部读一遍并处理好;再是核心逻辑函数的空壳,参数和数据容器都定义好;最后是输出部分,把格式串先写好,再用占位符跑通流程。框架通了再往核心函数里填细节。

这样做的最大好处是心理上的,看到程序能完整跑完输入输出,哪怕答案是错的,心态也是稳的,调试时能从“全盘崩坏”缩小到“某一步逻辑错”。另一个好处是,如果题目变成多组输入,你的框架天然支持,不用二次改造。

3.3 样例过了不等于AC:五步自查清单

提交之前花两分钟过一遍自查清单,能救回不少无谓的WA。我自己的清单有五项,全部压在记忆里:

第一,边界值。l等于1或r等于n、n等于1、树为空、路径不存在,这些最容易炸。第二,多组数据之间的残留。全局数组、容器、计数器在下一组数据开始前必须清空,vector要resize,map要clear。第三,输出格式。行尾有没有多余空格,每行末尾有没有换行,多组输出之间有没有空行,全部核对一遍。第四,类型与范围。INF是否是0x3f3f3f3f,乘法是否会溢出int,要不要开long long。第五,题眼复读。再念一遍题,确认自己有没有漏掉“严格”“非递减”“从1开始编号”这种修饰。

4. 高频踩坑记录与排查技巧实录

4.1 运行时错误对照速查表

机试评测结果就那么几种,每种背后都对应一个常见的代码错误。我整理一张速查表,遇到报错直接对着排查,能节省大量时间。

评测结果常见原因排查方向
Compile Error变量名写错、少头文件、语法错误看编译信息,重点检查函数签名和类型匹配
Runtime Error数组越界、除以零、栈溢出、空指针检查循环边界、动态内存释放、递归深度
Time Limit Exceeded算法复杂度过高、死循环、输入输出太慢确认数据范围与算法是否匹配,关同步流
Wrong Answer边界条件没处理、输出格式错、读入错位用极端样例自测,重新读题
Presentation Error输出多余空格/换行,与标准答案格式不符逐字符对比输出格式,尤其是行尾空格

4.2 我踩过的三个典型坑

第一个坑是全局数组残留。有次练习多组输入的Dijkstra题,第一组数据跑得好好的,第二组开始路径错乱,查了半天发现是graph[]没清空,上一组图的边全残留了下来。从那以后我养成了习惯:所有全局容器在while循环开头统一clear(),并且和输入操作写在一起,防止漏掉。

第二个坑是路径输出忘了reverse。第一次写Dijkstra带路径时,回溯完顺直接输出路径数组,结果是倒序的。检查了二十分钟才反应过来。其实这个错误特别好防:路径回溯必然是先得到终点再得到起点,要么原地reverse,要么用递归从起点开始打印,二选一写熟练就行。

第三个坑是表达式求值里的负数。第一次自己写中缀表达式求值,样例全是正数,一切正常。结果加了-3+2这种用例直接崩溃,因为操作数栈是空的,pop了不存在的元素。后来学了补0大法才彻底解决。这里提醒一句:机试题目描述里没说没有负数,你就要默认它可能会有,考点往往就藏在这种不起眼的地方。

4.3 考前两周的复习节奏安排

如果从现在开始算,距离考试还有两周,我的建议是不要再去学任何新算法了,把复习重心压回高频模板和老题重做。第一周,每天固定刷三到四道高频类型题,链表、二叉树、最短路、DP、并查集、模拟题轮着来,每道题都必须完整提交AC,不允许在IDE里看完输出就算完。第二周进入全真模拟状态,每天下午卡时间做一套完整题目(4到6题),统一按两个小时计,中间不查资料、不暂停,模拟考场的紧张感。

每天睡前花半小时背模板,不是背代码,而是背每个模板的适用条件和边界坑。比如LIS的lower_bound和upper_bound分别对应什么条件、带权并查集的合并公式怎么推导、Dijkstra的堆节点什么时候跳过。这些知识点在两小时的考场高压环境里不可能现场推导,靠的就是考前反复念到形成条件反射。

我个人在实际操作中的体会是,机试复习最忌讳的其实是“自我感动式刷题”,每天刷十几道却全是重复确认自己会的,真正薄弱的地方永远不碰。最好的备考节奏反而是每做一道题,都问自己一遍:这题考察的核心是什么?我能不能不看模板徒手写出来?如果两个问题里有一个答案是否定的,这道题就不算过关,必须重做。把这六类基础题吃透,哈工大机试至少不会成为你复试中的短板。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询