☰
从杭电到华为:一次OJ刷题日志中的边界与细节
2026/9/26 17:49:32 网站建设 项目流程

3月11号晚上,我照常打开题单准备刷几道OJ题。原本只是想热热身,结果从杭电的入门题一路点到了某高校OJ的智能指针题,从大数加法写到了字符串压缩,从简单模拟调到了递归边界。那晚结束之后我忽然意识到,这一整天的题单组合,其实刚好串联起了一个很完整的观察:OJ刷题这件事,真正考验人的从来不是算法天赋,而是你对待细节和边界的态度。

这个标题里的"3.11"不是题目编号,是我自己刷题日志里的一天。当时我一边刷一边把这天涉及的题号、平台、常见报错和思路全部记了下来,后来整理成了一套可复用的刷题方法。这里就把3月11日这一天里我实际刷过的题、踩过的坑、以及从中学到的东西完整写出来,给正在OJ入门、准备机试或者纯粹想系统训练算法基础的你一个参考。

1. 3月11日题单的梯度设计:从杭电到华为,从水题到工程题

先说我这一天实际接触的题单构成。热词里反复出现的杭电OJ、华为OJ、东方博宜OJ、湘潭大学OJ、江南OJ,本质上都是同一个东西——在线评测系统,只是归属的平台不同。杭电OJ在ACM圈子里属于元老级平台,题量大、题目经典;东方博宜OJ在信息学竞赛入门阶段很常用;华为系OJ更多面向企业机试,风格偏工程应用。它们共用一套"提交代码-在线判题-返回AC/WA/RE/TLE"的逻辑。

我那天处理的题大概分成四个梯度:

题号/关键词平台题型难度定位
杭电OJ 1002HDU大数加法(高精度)入门偏基础
杭电OJ 1020HDU字符串连续字符统计入门偏字符串
杭电OJ 1096HDU多case求和 + 输出格式入门偏格式
东方博宜OJ 1065 / 1168东方博宜偏递归/数列/数组综合从入门到进阶的过渡
华为OJ / 华为OD机试华为在线评测字符串处理、边界逻辑工程型题目
西北农林科技大学OJ + 智能指针高校OJ现代C++语言特性课程型进阶题

刚开始刷OJ的人最容易犯的一个错误,就是觉得"题目简单就等于可以秒杀"。但实际上杭电OJ 1002这道经典题,每年都有大量新手挂在输出格式上——算法本身五分钟能写完,格式错误能卡你两小时。我3月11号的第一课就是:简单题考察的不是智商,而是你是否愿意静下心来把规则完整读完。

刷题计划怎么安排才合理?我的经验是不要"一天刷十道不同类别的题",而是"一天一个专题"。像我这一天虽然看起来跨了好几个平台,但其实可以拆成三个专题:高精度计算(1002)、字符串与格式控制(1020/1096)、进阶题型思维(东方博宜的递归题、华为的风格题、智能指针题)。一天处理三组不同类型的题目,每组两到三道,既不会疲惫,又能让大脑在不同思维模式之间切换。时间安排上,我习惯早上处理需要动脑的字符串题,下午做数学向的高精度和递推,晚上留给复盘和整理模板。

2. 杭电OJ 1002:大数加法真正难的不是加法,是细节

杭电OJ 1002(A + B Problem II)大概是所有ACM入门者共同的记忆。题目描述很简单:输入若干组大整数A和B,求它们的和。表面上看就是加法,但陷阱全在细节里。

为什么"大数加法"这个知识点在网上被反复讨论?因为计算机内置的整数类型有范围上限,int大概到21亿,long long到9×10^18,而OJ题目里的A和B最长可以达到上千位。用内置类型根本存不下,所以必须用字符串模拟手算加法的过程:从个位开始逐位相加,满十进一。整个过程像极了小学列竖式——这也是高精度算法最核心的思维模型。

我3月11号当天写这个题时踩了一个非常蠢的坑。先说我最终通过的模板,再解释每个关键点:

#include <iostream> #include <string> #include <algorithm> using namespace std; string addStrings(string a, string b) { string result = ""; int i = a.size() - 1, j = b.size() - 1; int carry = 0; while (i >= 0 || j >= 0 || carry > 0) { int sum = carry; if (i >= 0) sum += a[i--] - '0'; if (j >= 0) sum += b[j--] - '0'; carry = sum / 10; result.push_back('0' + sum % 10); } reverse(result.begin(), result.end()); return result; } int main() { int n; cin >> n; for (int caseNo = 1; caseNo <= n; caseNo++) { string a, b; cin >> a >> b; cout << "Case " << caseNo << ":" << endl; cout << a << " + " << b << " = " << addStrings(a, b) << endl; if (caseNo != n) cout << endl; } return 0; }

这段代码里有几个位置必须注意。

第一,为什么从字符串末尾往前遍历?因为加法的进位是自低位向高位传递的,字符串存储大数时第0位是最高位,所以必须倒序遍历,这才是"竖式计算"的原生顺序。

第二,为什么循环条件是i >= 0 || j >= 0 || carry > 0?因为两个数字长度可能不同,较短的加完了之后还得继续处理较长的剩余位;就算两个都加完了,如果最后一步产生了进位,这个进位不能丢。漏掉carry > 0这个条件,会导致999 + 1算出来少了最高位的1。

第三,结果在push_back过程中是从低位到高位排列的,所以最后必须reverse。忘了反转字符串,得到的就是一位一位反着的结果,比如"12 + 34 = 64"之类的诡异输出。

第四,输出格式。杭电OJ 1002要求每组输出Case x:加一行算式,且两组输出之间有一个空行,但最后一组后面不能多出空行。这就是我用if (caseNo != n) cout << endl;的原因。我那天第一次提交WA,就是因为在每组后面都无条件加了空行,导致最后多打一个换行触发了Presentation Error。这种"最后一个case不输出空行"的规则,在OJ系统里几乎是通行的潜规则,大家一定要形成条件反射。

还有一些细节值得展开说。比如加法的进位结果直接通过sum / 10取得,sum % 10作为当前位,这个方法比显式判断if (sum >= 10) carry = 1更快更不容易漏。再比如字符和数字的转换,a[i--] - '0'是标准做法,不要自作聪明用atoi,一次只处理一个字符用减'0'最简单。我见过太多初学者在这些地方写出一堆冗余代码,反而是错因。

2.1 大数加法的常见报错和Debug思路

我那天调试1002时,系统返回过三种典型错误,这里直接列出来供对照:

  • WA(Wrong Answer):通常是进位没处理干净。测试用例试一下999 + 1、0 + 0、1000 + 9999,分别看看结果是否正确。这三个用例能覆盖大部分进位和长度差问题。
  • PE(Presentation Error):输出格式和答案只差在空行或空格。这种情况在OJ里最气人,逻辑全对但格式不对。我的建议是完完全全逐字对照题目给的Output样例,连空格数量都不要放过。
  • RE(Runtime Error):如果用了数组而不是字符串,可能是数组开小了。1000位的数字至少要开1005长度的数组,还要留一位给进位。字符串则没有这个问题。

Debug大数题有个很实用的技巧:不急着提交,先自己写一个循环,把1到9999里所有A + B的加法全部跑一遍,和内置类型计算的结果逐一对比。一旦有不一致,立刻就能定位到进位逻辑的问题。这个思路我后来用在所有高精度题上,AC率提升非常明显。

3. 杭电OJ 1020和1096:字符串压缩与输出格式,考的全是边界

热词里"杭电oj 1020"和"杭电oj 1096"都排在前列,说明这两道题是很多人的共同记忆点。它们恰好代表了OJ刷题里最常见也最阴险的两类坑:字符串边界处理和输出空行控制。

3.1 杭电OJ 1020的连续字符统计

1020这道题给一个字符串,要求统计连续相同字符的数量,并输出"字符+次数"的形式。比如AABBB应该输出A2B3。核心逻辑不复杂:遍历字符串,遇到和上一个字符相同的就计数加一,遇到不同的就把上一个字符的结果输出。

但有一个边界特别容易翻车——字符串遍历到末尾时,最后一个连续的字符段不会被结算。很多新手的代码长这样:

for (int i = 0; i < str.size(); i++) { if (str[i] == lastChar) { count++; } else { cout << lastChar << count; lastChar = str[i]; count = 1; } }

这个版本里,如果字符串是AABB,遍历结束后最后一段BB只更新了count,却没有输出B2。正确做法是在循环结束后,把最后一段单独补一次输出:

for (int i = 0; i < str.size(); i++) { if (i == 0) { lastChar = str[i]; count = 1; } else if (str[i] == lastChar) { count++; } else { cout << lastChar << count; lastChar = str[i]; count = 1; } } cout << lastChar << count; // 关键的一行

我当时就是漏了最后一行,白白交了两发WA。这类"最后一个元素结算"的边界,在数组遍历、字符串处理、二分查找里都会反复出现。我的建议是写循环前先在纸上把长度为1和长度为2的用例走过一遍,确认循环结束时的状态是什么,再决定循环结束要不要补一段收尾逻辑。

还有一个小细节:题目里字符串是纯大写还是大小写混合,决定了统计时是否要区分大小写。我3月11号刷到的版本明确要求按原字符统计,也就是大写和小写当作完全不同的字符计数,不能合并。这类题目要求一定要在动手前看清楚。

3.2 多case输出空行的判断逻辑

杭电OJ 1096属于经典的A+B多case系列,题目本身只是求和,难点同样在输出:每个case的结果占一行,case之间要有一行空行,但最后一个case后面不能带空行。解决方案是我在1002里已经用过的写法:判断当前case是不是最后一个,不是才输出空行。

for (int i = 1; i <= cases; i++) { int a, b; cin >> a >> b; cout << a + b << endl; if (i < cases) cout << endl; }

就这五行代码,每年卡住的初学者数量惊人。我记得有个统计数据,这类多case格式题在所有OJ提交里的PE占比极高。原因是很多人脑子里只有"每两个case之间空一行"这个概念,却忘了检验"最后一行后面不能有换行"这个隐藏规则。实际上,做一个简单的归纳就能解决:只要把空行看成是"每个case前面的分隔符"而不是"每个case后面的输出内容",写成"除了第一个,每个case前先输出一个空行",就永远不会在结尾多出空行。

if (i > 1) cout << endl; cout << a + b << endl;

我3月11号之后就把这个模式固定下来,凡是遇到多case输出的题,一律采用"第一个不打印分隔,之后的先打印分隔再输出内容"的逻辑。这个思路在OJ之外也很有用——日志记录、报表输出、命令行的分隔符打印,都是同一个套路。

4. 东方博宜OJ 1065和1168:从"搜答案"到"拆思路"

热词里"东方博宜oj答案1065"和"东方博宜oj答案1168"这两个搜索词,说实话让我有点感慨。东方博宜OJ面向的大多是信息学启蒙阶段的学生,题目难度并不算高,但题号1065和1168这两个位置,恰好是从单纯的"循环套用"迈向"综合思维"的过渡区。搜"答案"的人多了,说明很多人卡住的不是语法,而是"不知道题目在问什么"。

我必须诚实地说一句——我没有把这两个题号逐字背下来,所以就不在这里"默写题面"了。但根据东方博宜OJ的出题习惯,1065和1168这类编号中段的题目,大概率落在递归数列、数组综合处理或者简单递推上。你要做的是打开题目,把输入输出样例读三遍,看清楚三个问题:数据范围多大、边界条件是什么、是否存在多case输入。这三点清楚了,思路往往就出来了。

4.1 数列递推题的通用思维框架

如果你现在正卡在某道东方博宜的递归或数列题上,别急着搜答案,先试一下这个框架。

第一步,把递推关系写出来。比如斐波那契类是f(n) = f(n-1) + f(n-2),阶乘是f(n) = f(n-1) * n,前缀和是f(i) = f(i-1) + a[i]。凡是能写出"当前结果由之前结果计算得到"的,都是递推。

第二步,确定初始值。递推必须有一个起点,比如f(0)=0, f(1)=1,或者f(1)=1。初始值缺失或错误,是整个递推链条崩溃的最常见原因。

第三步,考虑是递归还是循环实现。递归代码结构清晰,但n较大时会爆栈;循环直接迭代没有这个问题。入门阶段我建议优先用循环,等理解了状态转移之后再回头看递归,会豁然开朗。

// 通用递推模板:从底往上算 // 假设f[i]表示第i项结果 vector<long long> f(n + 1, 0); f[1] = 1; // 初始值,按题目要求设置 for (int i = 2; i <= n; i++) { f[i] = f[i - 1] + f[i - 2]; // 递推式按题目要求替换 } cout << f[n] << endl;

这个模板能解的题远比你想得多:爬楼梯、铺砖块、兔子繁殖、约瑟夫环的递推版本,核心都是"找到f[i]和之前项的关系"。所以我看到"东方博宜oj答案1168"这种搜索词时,第一反应不是"把代码给出来",而是"把这类题的统一解法讲明白"。一个能复制到十道题上的思路,比一道题的完整代码有价值得多。

4.2 为什么"看思路"比"看答案"管用

我在3月11号的复盘里专门写下了一条:搜答案搜来的代码,你第二天就忘了;自己推出来的思路,三个月后还能用。这背后的原因是,OJ题目的价值不在那个AC,而在你大脑里建立的条件反射——看到"数据范围很大"想到高精度,看到"连续字符"想到双指针或逐段统计,看到"递推关系"想到边界与初始化。这一整套反射,靠抄答案永远建立不起来。

我的建议是给自己设一个"20分钟规则":一道题如果20分钟还没有任何头绪,允许看题解,但只看思路部分,不看完整代码。然后用脑子里的思路自己写一遍,写不出来的地方再回头对照。这个过程比直接抄代码痛苦,但效果是立竿见影的。3月11号那天我处理东方博宜的题就是这么干的,虽然多花了一个小时,但那个递推边界问题从此再没犯过。

5. 华为OJ和高校OJ的智能指针题:刷题刷的不该只是AC

如果只看杭电和东方博宜,你可能会觉得OJ刷题就是算法题。但热词里"华为oj""华为oj编程"以及"西北农林科技大学c++ oj智能指针"的出现,说明OJ这个生态还有另一个重要分支——面向工程能力考核的在线评测。

5.1 华为系OJ题型的三个典型特征

华为系在线评测(很多人习惯叫华为OJ)和传统ACM题目的感觉不太一样。传统ACM题喜欢纯粹的算法和数据结构,而华为系题更爱"字符串解析+条件分支+状态模拟"的组合拳。我总结下来有三个典型特征:

  • 输入格式复杂:一行里有多个字段,中间可能有逗号、分号、方括号,需要自己写解析逻辑。
  • 边界条件特别多:题目会把"一种边界情况"包装成"完整的故事背景",你要在细节里找到真正的判定条件。
  • 输出要求极其严格:不仅结果要对,格式、顺序、甚至相同结果时的输出先后都有讲究。

应对这类题,我建议养成一个习惯:先搭好输入处理框架,再写业务逻辑。把数据一行一行读进来,做结构化,再进入判断流程。很多人一上来就写核心逻辑,结果输入都没接住,后面全白搭。

5.2 OJ题里到底该不该用智能指针

热词"西北农林科技大学c++ oj智能指针"值得单独聊聊。高校OJ把智能指针放进题目,说明计算机基础教育已经开始从中级C++迈向现代C++了。智能指针std::unique_ptr、std::shared_ptr、std::weak_ptr解决的核心问题只有一个:内存泄漏和悬垂指针。

#include <memory> std::unique_ptr<int> p = std::make_unique<int>(42); // 离开作用域时自动释放内存,不需要手动delete std::shared_ptr<int> sp1 = std::make_shared<int>(100); std::shared_ptr<int> sp2 = sp1; // 引用计数为2,全部离开作用域后释放内存

在纯算法题里,智能指针确实用得不多——算法题一般不需要你管理堆内存,数组和容器就够用了。但在课程作业、工程项目和华为这类偏应用的机试题里,智能指针考察的是你有没有"资源管理"的意识。new了之后忘了delete,就是典型的C++内存泄漏;而unique_ptr直接把所有权表达得清清楚楚,代码既安全又容易读。

我3月11号刷到智能指针相关题时,第一反应是"这题怎么不在算法范围里"。后来想明白了:很多高校的OJ不只是算法训练平台,还是课程验收工具。程序设计课教到智能指针,OJ自然会出现对应的题目。如果你现在要考这类课程题,建议把三件事练熟:make_unique和make_shared的标准写法、所有权转移std::move的语义、以及shared_ptr循环引用时如何用weak_ptr打破环。这三个知识点是智能指针题最常见的考点。

从工程能力角度看,华为OJ和智能指针题其实指向同一个信号:现在的机试不再只考"你会不会算法",也开始考"你写出来的代码放在真实项目里靠不靠谱"。所以我的态度很明确——刷题时多想想代码的健壮性和可读性,适度使用现代C++特性,这些习惯在机试里不会吃亏,在真实项目里更是直接受益。

6. 3月11日复盘清单:一套可以反复使用的刷题沉淀方法

每次刷完题,真正决定你进步的环节是复盘。3月11号晚上我做了三件事:整理错题表、提炼可复用模板、标记需要二刷的题目。下面是我几次迭代后固定下来的复盘格式,直接抄作业就能用。

6.1 错题复盘表格

我习惯在本地维护一个Markdown表格,每道错题占一行,字段如下:

题号错误类型根因分析解决方案同类题标记
HDU 1002PE最后一个case后多输出了空行采用"第一个不打印,之后先分隔再输出"所有多case输出题
HDU 1020WA字符串末段未结算循环结束后补一次输出所有分段统计类题
东方博宜XXXWA递推初始值设错先手算前3项验证递推式所有递推题

这个表最重要的不是记录错误本身,而是把错误归类。我做了几个月之后发现,常见的OJ错误其实就那么几类:进位/借位没处理干净、边界元素未结算、输出格式不符合规范、数组越界、数据范围超过int。当你发现错题在这五类里反复出现,就知道自己该针对性地练哪一块了。

6.2 三次重复策略

从哪看到这个我不记得了,但实践下来确实有效:一道做错的题,当天晚上不看代码重写一遍,三天之后再写一遍,一周之后再写一遍。三次都能独立通过,才算真正掌握。这个策略看起来很笨,实际上比盲目刷十道新题高效得多。因为每次重写,你的大脑都会重新走过一遍当时的bug和修复路径,把"错误模式"和"正确解法"牢牢绑在一起。

3月11号那天我特别注意了这一点——当时有两道题卡到晚上十点半也没完全AC,换了以前我可能就直接看题解抄过去了。但这次我忍住了,把题号和我的思路写进表格,标记为"待二刷"。三天后我重新打开那两道题,果然还是ak一个,但这次调试只花了十五分钟就通过了。那种感觉,比一百次搜到答案都爽。

6.3 关于搜"OJ答案"这件事的最后一点体会

打从开始写这篇东西,我就一直在想"东方博宜oj答案1065"这类搜索词背后的人到底需要什么。说实话,谁没有搜过答案呢?我自己在入门阶段也没少搜。但我渐渐发现,真正让我进步的,从来不是复制下来的那段AC代码,而是我在理解代码时想通的那个"为什么"——为什么这里要加一,为什么那里要从后往前扫,为什么循环结束之后还要补一次处理。

所以如果你正卡在某个OJ题号上,我的建议是:先只搜题目大意和思路,不看代码。自己写一遍,卡住了再去搜半个代码片段,然后继续写。这个过程中你获得的东西,远比一个AC记录值钱。

3月11号这个日期,在这篇日志里已经不只是一个时间点了。它是我把"刷题"从任务变成方法的一个锚点。杭电的格式题教会我谨慎,东方博宜的递推题教会我把问题抽象成模型,华为OJ的工程型题目教我关注代码的健壮性,智能指针题提醒我别只盯着算法数据结构,忘了语言本身也在进化。下一次你再看到某个日期配上OJ三个字母,别只想到"今天又要提交作业",试着把这一天当成一次完整的训练切片——认真做一遍,认真复盘一遍,认真记下来。这比一次多刷十道题的收获,要大得多。

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

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

立即咨询