DHU上机打卡D7,这个标题在我备忘录里躺了整整一周。第七天,我照例在东华大学OJ上刷完三题、截图、写复盘,然后关掉终端。坚持到这一天,最明显的变化不是会做的题变多了,而是看到WA不再烦躁,能按步骤排查出问题在哪。这篇就把D7当作一个切片,讲清楚我这七天是怎么规划的、D7当天具体做了哪几道题、踩了哪些坑,顺带聊聊这些练习对准备华为OD机考和考研复试上机到底够不够用。如果你也想把上机刷题这件事坚持下来,或者正在为机考、复试发愁,这份复盘应该能给你一份可以直接上手的路线。
1. D7打卡的起因与这七天的整体安排
1.1 为什么会选DHU OJ作为打卡阵地
先说背景。我之前刷题最大的问题是“刷得散”:今天LeetCode两道题,明天牛客一道,后天换个平台再做几题,看起来天天在学,但一个月后回头一查,真正吃透的题型没几个。后来我给自己定了个规矩:固定一个OJ,固定一个周期,每天至少AC三道题,并且把每道题的思路、复杂度、踩坑点写成打卡笔记。选择东华大学OJ,理由很直接:题目分类比较清楚,从基础到进阶的跨度合适,提交反馈够快,而且不用像某些商业平台那样开会员才能看完整解析。
可能有人会觉得,现在大家不都用LeetCode和牛客吗,为什么回学校OJ刷题?我的体会是,学校OJ的题目风格更贴近真实考试和上机环境:输入输出要自己处理,多组测试用例是常态,题目描述有时候还会故意留点模糊地带。这种“不友好”其实是在帮你提前适应考场。尤其是准备复试上机的同学,很多院校的上机题就是这种画风,不提前适应,考场上很容易被输入输出格式搞得心态崩掉。
1.2 七天路线:从语法热身到图论动态规划
七天的路线我在第一天就规划好了,免得每天临时想练什么。前两天先做基础题,把输入输出、循环、数组、字符串这些容易手生的部分热起来;三四天集中在排序、二分、双指针;五六天开始碰树和图,主要是DFS、BFS和最小生成树;第七天做一次综合复盘,把前面几类题型各抽一道重新做一遍。
| 天数 | 主题 | 目标 |
|---|---|---|
| D1-D2 | 输入输出、模拟、字符串基础 | 找回写代码的手感,AC至少6题 |
| D3-D4 | 排序、二分、双指针 | 掌握常见复杂度优化套路,AC 8题 |
| D5-D6 | DFS、BFS、图论基础 | 能独立写出搜索模板,AC 8题 |
| D7 | 综合复盘 | 重做经典题,记录踩坑清单 |
这张路线表看起来简单,但执行起来最大的难点不是题目难,而是“每天固定投入时间”。我建议不管当天状态如何,至少开编译器跑一题再收工,哪怕只过一题也算打上卡。习惯一旦断了一天,后面很容易彻底断掉。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. D7当天实测的三道典型题目复盘
D7当天我没有去碰新题,而是把前六天最容易出错的三类题各抽了一道重写:大数加法、二叉树层序遍历变体、0/1背包状态压缩。这三题分别代表字符串处理、树的BFS、经典动态规划,刚好也是各类上机考试的高频区域。
2.1 大数加法:基础中的基础,但OJ上并不好过
很多同学看到“大数加法”会觉得很无聊,觉得不就是把两个数加起来吗?但在OJ上,数字范围经常超出long long,比如两个1000位的十进制数相加,这时候只能用字符串模拟竖式。我D7重写这题时,第一版没有处理多组输入,结果本地样例过了,提交直接Output Limit Exceeded,光这个就浪费了两次提交。
cpp复制#include <iostream>
#include <algorithm>
#include <string>
using namespace std;
string addBig(const string &a, const string &b) {
int i = (int)a.size() - 1;
int j = (int)b.size() - 1;
int carry = 0;
string res;
while (i >= 0 || j >= 0 || carry) {
int sum = carry;
if (i >= 0) sum += a[i--] - '0';
if (j >= 0) sum += b[j--] - '0';
carry = sum / 10;
res.push_back(char('0' + sum % 10));
}
reverse(res.begin(), res.end());
return res;
}
int main() {
string a, b;
while (cin >> a >> b) {
cout << addBig(a, b) << endl;
}
return 0;
}
代码本身不复杂,但有几个细节值得注意。第一,字符转数字一定要减掉'0',反过来数字转字符要加上'0',这个低级错误我在D2犯过,当天花了不少时间才找到。第二,如果输入允许“0001”这种前导零,输出时最好去掉,否则某些严格比对输出的题目会判Presentation Error或者Wrong Answer。第三,双循环里必须把i >= 0 || j >= 0 || carry写全,否则最高位进位会丢。
2.2 二叉树层序遍历变体:BFS模板的灵活运用
D7第二题我做的是“求二叉树每一层节点的平均值”。第一次做类似题时,我是先层序遍历存下所有节点的层号,再按层汇总,代码写得又臭又长。后来才意识到,BFS天然适合按层处理,只要在每一轮开始前记录当前队列的大小sz,这一轮只需要弹出sz个节点,就能精准控制一层。
cpp复制struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
vector<double> averageOfLevels(TreeNode* root) {
vector<double> ans;
if (!root) return ans;
queue<TreeNode*> q;
q.push(root);
while (!q.empty()) {
int sz = (int)q.size();
double sum = 0;
for (int i = 0; i < sz; ++i) {
TreeNode *node = q.front();
q.pop();
sum += node->val;
if (node->left) q.push(node->left);
if (node->right) q.push(node->right);
}
ans.push_back(sum / sz);
}
return ans;
}
这里有个隐藏坑:如果二叉树节点的值很大,double sum在某些极端数据下还是会溢出。比较稳妥的做法是用long double,或者先算平均值再累加。虽然OJ样例大概率不会卡到这里,但上机考试里多考虑一步总是好的。
这道题给我的启发是:BFS模板不要死记硬背,而是要理解“当前队列里的元素就是下一层之前的全部节点”这个本质。理解了这一点,层序遍历、每层最大值、之字形遍历、二叉树右视图,几乎可以在同一个模板上改出来。
2.3 0/1背包的状态压缩:一维DP为什么必须倒序
D7第三题是经典0/1背包:有N个物品,每个物品有重量w[i]和价值v[i],背包容量为V,问能装下的最大价值。二维DP的状态转移很直观:dp[i][j]表示前i个物品在容量j下能获得的最大价值,递推式是dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])。
但上机考试时间有限,二维数组有时候会超内存,所以必须会写滚动数组版本:
cpp复制int knapsack(int N, int V, vector<int>& w, vector<int>& v) {
vector<int> dp(V + 1, 0);
for (int i = 0; i < N; ++i) {
for (int j = V; j >= w[i]; --j) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
return dp[V];
}
为什么要倒序?因为0/1背包要求每个物品最多选一次。正序遍历时dp[j - w[i]]已经在当前第i个物品这一轮被更新过,再取它就会导致同一个物品被选多次,那就退化成了完全背包。倒序遍历时,dp[j - w[i]]还是上一轮的结果,对应的是“前i-1个物品”的状态,这样才符合0/1背包的定义。
我还想提醒一个不常被提到的细节:如果题目要求“恰好装满背包”的最大价值,初始化就不能全为0,而是要让dp[0] = 0、dp[1...V] = -INF。很多同学直接套“不超过容量”的模板,导致这种题一分都拿不到。D7复盘时我把这两种初始化的区别写进了笔记,后面遇到类似的变体就再也没翻过车。
3. DHU上机环境与OJ提交最容易翻车的几个细节
代码本身写对了,不代表能一次AC。上机环境里的输入输出、编译器版本、运行内存这些坑,比算法本身更能决定成绩。D7这天我专门花了半小时把所有提交记录翻了一遍,总结出三个高频翻车点。
3.1 多组输入与EOF:读入方式决定你能不能过样例
东华大学OJ这类传统OJ,很多题目都要求“多组输入,处理到文件结束”。也就是说,你不能只读一组数据就结束,而是要用while (cin >> a >> b)这类写法循环读入。用Python的话,常见做法是用sys.stdin.read()一次性读入全部数据再切片解析,或者用sys.stdin.readline()配合while True判断是否读到空行。
我见过不少同学在本地IDE里跑样例完全没问题,一提交就Output Limit Exceeded,原因就是没有处理多组输入,程序读了一组数据后正常退出,但评测系统还在等程序继续读后面的数据。这个问题在D1就坑过我一次,之后我拿到任何题目都会先看一眼题目描述里有没有“多组输入”“EOF”字样。
还有一点:输出格式里每个答案后面换行,但有些题目不要求最后一个答案后面多一个空行,而有些题目要求两组输出之间加空行。这类格式问题最好的应对办法不是硬记,而是把样例输出复制下来,用文本比对工具检查一遍自己的输出,确认没有多余空格或换行再提交。
3.2 编译器版本、容器选择与运行时报错
学校OJ的编译器版本往往比你本机旧。比如本机默认C++17,OJ可能只开C++11甚至C++98。如果你用了C++11之后的特性,比如auto、stoi、to_string,老编译器会直接编译失败。我现在的做法是:正式提交前先看一眼OJ页面支持的语言版本,优先选比较新的标准;如果只能选老标准,就老老实实把auto换成显式类型,把to_string换成sprintf或手工转换。
运行时报错(Runtime Error)也是上机考试里的常客。最常见的三个原因:数组越界、除数为零、递归导致栈溢出。说白了,OJ不会告诉你具体是哪种错误,只会显示一个笼统的RE。这时候不要瞎猜,直接检查所有下标访问,尤其是循环边界;再查一下有没有对可能为0的数做除法;如果用了递归,看看数据规模是不是大到会爆栈。
3.3 时间限制与内存限制的估算法
决定用哪种算法,不能光靠感觉,得会算数量级。我常用一张表来快速判断:
| 数据规模 | 建议时间复杂度 |
|---|---|
| n ≤ 10 | O(n!) / O(2^n) |
| n ≤ 20 | O(2^n) |
| n ≤ 10^3 | O(n^2) |
| n ≤ 10^5 | O(n log n) |
| n ≤ 10^7 | O(n) |
以C++为例,1秒内大概能执行10^8次简单运算,所以看到n=10^5第一反应就应该是“能不能用O(n log n)”,而不是直接写O(n^2)。内存方面,一个int类型的数组,开10^6个元素大约占4MB,开10^7个就是40MB,很多OJ的内存限制是128MB或256MB,超了会直接MLE。建议在本地写代码时提前估算数组占用,别等到提交了才发现内存超限。
4. 对标华为OD机考与复试上机:D7练的东西够不够用
把七天打卡的练习内容放到真实考试里,到底够不够?这是我在D7复盘时最关心的问题。毕竟刷题不能只为了打卡,还得服务目标。
4.1 华为OD机考的题型分布与评分逻辑
华为OD机考是很多技术岗位入职前的上机环节,通常包含2到3道编程题,总时长在120分钟到150分钟左右。常见分值结构是100分+200分,或者100分+100分+200分。题型主要集中在字符串处理、数组与逻辑模拟、贪心、DFS/BFS、动态规划这五类,整体难度不会特别难,但题干通常特别长,输入输出描述也不像学校OJ那么规范,需要你自己从一大段文字里提取字段和规则。
评分逻辑是按测试用例的通过比例给分的,不是“全对才有分”。哪怕你只写出一个暴力解、只能通过30%的用例,也可能拿到这30%的分数。所以我的建议是:遇到没思路的题,先写一个最朴素的暴力解法交上去,保住基础分,再考虑优化。这一点和平时在OJ上追求100%通过的习惯不太一样,却是机考拿分的重要策略。
4.2 复试上机和日常OJ刷题的真实差异
复试上机的情况因学校而异,但总体来说比华为OD机考更偏基础,一般2到3道题,考察C/C++、算法与数据结构基本功,难度大概在LeetCode中等偏下。东华大学这类院校的复试上机,常见考点包括链表操作、二叉树遍历、排序、查找、字符串、简单图论,不会刻意出特别偏的题。
但复试上机和日常刷题有两个差异值得注意。第一,考场上没有“提交-反馈-再提交”的快速循环,通常只有几次提交机会,或者只能看到部分反馈,所以平时养成先静态检查代码的习惯非常重要。第二,复试上机可能考察手写代码的规范度,比如变量命名、缩进、注释,这在校外OJ上完全不会影响得分,但在人工阅卷场景下会成为印象分。D7打卡结束时,我重新检查了自己这几个月的代码风格,把不规范的写法改了过来。
4.3 从D7出发的双周补强计划
如果目标是华为OD机考,光靠七天基础打卡还不够,我给自己排了一个双周补强计划:
- D8-D10:集中刷字符串处理题,包括大数运算、进制转换、字符串分割与匹配、简单正则模拟。
- D11-D12:主攻DFS和BFS,题目从网格类搜索做到图的最短路径,重点练BFS求最短路这个套路。
- D13-D14:动态规划专题,先过一遍0/1背包、完全背包、最长递增子序列、区间DP这些经典模型,再练几道一维状态转移的题目。
- D15:全真模拟,找一套完整的机考真题,按真实时长限时完成,强制自己用文本编辑器写代码再导入IDE编译。
如果你的目标是复试上机,可以把这个计划再往基础上压一压,把“链表”“二叉树遍历”“排序”这三个板块放到最前面,特别是要保证每道题都能用C/C++写出来,而不是只知道思路。
5. 七天踩坑实录:这些错误我几乎每天都犯
打卡七天,我最大的收获不是会了多少题,而是把自己反复犯的错误汇总成了一份checklist。下面几个坑,几乎每个蹲在OJ前的人都会遇到。
5.1 数组越界、边界用例与“样例过了但WA”的僵局
D3做一道循环移动数组的题,我在更新下标时忘了对长度取模,结果i=4、k=4时直接访问到8,数组越界。本地测试数据小,没爆出来,提交后报错半天找不到原因。后来我才学会一个笨办法:凡是涉及下标运算的地方,先拿边界值手推一遍,比如数组长度为1的情况、长度为0的情况、循环次数为0的情况。
很多WA其实都是边界条件没处理好。比如二分查找时目标值在左边界外、在右边界外;快速排序递归时传入的区间闭开写错;字符串转整数时空字符串或只有正负号。这些用例数量少,OJ里却往往专门准备着。我现在的习惯是:写完一题先自己补两组边界测试,n=0和n=max,再提交。
5.2 递归超时与记忆化:别把暴力当正解
D5做斐波那契数列的变体题,我上来就写了一个朴素递归,样例秒过,但提交后疯狂超时。原因很简单,朴素的fib(n)会重复计算大量子问题,复杂度是O(2^n)。上机考试里数据量稍微大一点,直接超时。解决办法是改成递推,或者加记忆化数组。
cpp复制int fib(int n) {
if (n <= 1) return n;
if (memo[n] != -1) return memo[n];
return memo[n] = fib(n - 1) + fib(n - 2);
}
这个坑的本质是:在没有分析复杂度的情况下,把暴力递归当成了正解。每次看到递归,我都强迫自己想一遍“这个状态会被重复计算吗”,如果会,马上换成递推或记忆化。另外,递归深度太深也会导致运行时爆栈,像DFS处理10^5数量的链式图时很容易踩中,这种场景要学会改成显式栈。
5.3 忽略编译器警告的代价
以前我总觉得编译告警只是“建议”,不影响运行就不管。D4那题把我教育了。当时写循环时把int i和str.size()直接比较,str.size()返回的是size_t无符号类型,编译器已经给了警告,但我直接忽略。结果str为空时,str.size() - 1变成极大数据,循环进入后立刻越界。这类问题在本地跑可能恰好没触发,到了OJ的测试数据上就原形毕露。
我现在写C++会让编译器把所有警告开到最严格级别:-Wall -Wextra。出现警告时必须先看明白含义,再决定要不要忽略。尤其是“有符号和无符号比较”“变量未初始化”“函数声明但没有定义”这几类,每个背后都可能藏着一个WA或者RE。
5.4 我的救命工具:本地对拍脚本
如果WA了又找不到原因,我最后会搬出对拍脚本。简单说,就是写一个绝对正确的暴力程序,再写一个优化后的程序,然后用随机数据生成器产生大量输入,同时跑两份程序并把结果逐行比对。只要有差异,立马就能定位出错用例。
python复制import random
import subprocess
import sys
for i in range(10000):
n = random.randint(1, 50)
with open('input.txt', 'w') as f:
f.write(f"{n}\n")
r1 = subprocess.run(['./brute'], stdin=open('input.txt'), capture_output=True, text=True)
r2 = subprocess.run(['./fast'], stdin=open('input.txt'), capture_output=True, text=True)
if r1.stdout != r2.stdout:
print("diff found at case", i)
print(open('input.txt').read())
print("brute:", r1.stdout)
print("fast:", r2.stdout)
sys.exit()
这个脚本看起来简单,但帮我找出的问题至少有三四个:质数判断漏掉2、二分边界写错、多组输入时状态没清空。如果没有对拍,靠肉眼盯代码,可能一晚上都定位不到问题。建议所有准备上机考试的同学都把这个工具装进自己的工具箱。
最后再分享一个小技巧:打卡D7之后,我会把每天WA的原因整理成一份checklist,写代码前先扫一遍。输入输出格式、边界值、初始化、递归复杂度、编译器警告,这五条永远是先于“算法思路”被检查的。如果你也在准备复试上机或者华为OD机考,不用纠结每天刷多少题,把D7这种打卡坚持到D30,变化一定看得见。我个人的体会是:把简单题做到不丢分,比到处找难题更有性价比。
