简介:这是一份杭州电子科技大学在线OJ平台一千至一千零九十九号题目的C/C++题解代码包,面向正在备赛、刷题或想巩固算法基础的编程学习者和竞赛选手。题目覆盖基础算法、数据结构、数学建模、逻辑推理等常见OJ考点,所有代码均调试通过,可直接运行对照。压缩包共九十个文件,以六十二个cpp和十六个c源文件为主体,并辅以pdb、dsw、ilk、exe等少量工程文件,整体约一点一MB,轻量精炼。资源已有一千八百三十五人学习浏览,是广受关注的OJ代码参考之一。通过研读这些实现,读者既能理解每道题的推导思路与边界处理,也能学习C/C++在指针、内存、结构体、函数调用等方面的典型用法,并观察不同优化策略在时间与空间上的取舍。文件名多包含题号与英文题目名,便于按需定位特定问题;配套的工程文件还展示了旧版Visual C++项目的组织方式,对学习传统OJ编程环境有一定帮助。无论是起步阶段的代码模仿,还是冲刺阶段的算法梳理,这套题解都能提供扎实的参考价值。
1. 杭电OJ的1000-1099题段:入门第一个AC,卡你的不是算法而是规则
杭电OJ(HDU OJ)的1000-1099题段,是许多刷题者第一次接触在线判题时走过的路。这个题段没有复杂的图论和动态规划,考的是三件基本功:把多组输入读完整、把输出格式写对、把最简单的数学和模拟题拆成代码。真正让新手反复提交不过的,几乎都不是“想不出解法”,而是不熟悉判题规则——EOF循环漏写、行尾多一个空格、数组少开几个位置,每个都让人翻车。这篇笔记以C为主要语言,兼顾Java和Python,把题段的题型套路、模板代码和五个高发坑一次讲清,适合刚开始刷OJ、以及刷了一半还在和PE/WA纠缠的读者照着复现。
2. 从 EOF 到多组数据:把读入规则写成条件反射
2.1 为什么 while(scanf(...)!=EOF) 是第一个要背下来的循环
OJ判题的本质是拿你的程序去跑一个输入文件,再把输出文件和标准答案逐字符比对。文件有结尾,这个结尾在C语言里表现为EOF。scanf每次调用返回的是成功读取并赋值的参数个数,当它读到文件末尾时返回EOF宏(通常是-1)。本地调试时,你从键盘敲入数据后程序永远不会主动结束,除非按下Ctrl+Z(Windows)或Ctrl+D(Linux),这也是很多人本地跑得好好的,一提交就超时或没输出的直接原因——他把读到文件结尾才退出的循环写漏了,程序只处理了一组数据就结束了。
#include <stdio.h> int main(void) { int a, b; /* 读不到数据时 scanf 返回 EOF,循环自然结束 */ while (scanf("%d%d", &a, &b) != EOF) { printf("%d\n", a + b); } return 0; }这段是题段里所有“多行输入,每行一组数据,处理到文件结束”题目的基础框架。scanf的返回值是关键:两个整数都读成功时返回2,只读到一个返回1,遇到非数字字符或文件结束返回EOF。用 != EOF 同时覆盖了“文件结束”和“读取失败”两种情况,循环不会带着未初始化的a、b继续往下算。有一点容易忽略:scanf遇到空白字符(空格、换行、Tab)会自动跳过,所以输入行首的空行、行尾多余的空格都不需要手工处理。
如果输入行里既有数字又有字符,比如类似“1+2=3”的格式,就别用%d一路读到底。常见做法是用%s读字符串再自行拆分,或者用fgets读整行、sscanf二次解析。这类题目在题段里出现频率不低,建议单独总结一个模板,而不是在每次提交前临时拼凑scanf格式串。
注意:有些编译器对EOF的警告让你写成 while(scanf("%d%d", &a, &b) == 2),这在仅处理数字时更严格,一旦输入混入非数字字符会直接退出。两种写法在题段常规数据下等价,按自己习惯固定一种即可。
2.2 T 组数据与“遇 0 结束”:循环模板怎么切换
另一类高频读入格式是:第一行是一个整数T,表示后面有T组数据。这时不能再套EOF循环,而是先读T,再用for控制次数。还有一类是“每行第一个数字表示本行有多少个数,读到0结束”,它同时用到了循环和退出条件。
#include <stdio.h> int main(void) { int t, a, b, i; scanf("%d", &t); /* 先读组数 */ for (i = 0; i < t; i++) { scanf("%d%d", &a, &b); printf("%d\n", a + b); } return 0; }用for而不是while,是因为组数已经明确给出,循环次数可控。这个模板的常见变形是“输入若干行,每行第一个数为n,接下来有n个数,遇到0行结束”,此时把for换成while(scanf("%d", &n) != EOF),并在循环体开头判断if (n == 0) break即可,后面所有数据都必须在这一组内读完。最忌讳的是把T组数据、EOF循环和遇0结束混在一个程序里:先读T,再while(scanf()!=EOF),会把下一道题的开头数据吞进来,这种错法在本地数据少时完全看不出来,像是玄学,一提交就错。
这类“混合读入规则”的题目,我一般先在草稿纸上标出数据流的边界:哪些数属于一组,一组从哪里开始到哪里结束,遇到什么条件要跳出。画清楚再写代码,比边写边试快得多,也少踩数组越界的坑。
2.3 行尾空格与组间空行:Presentation Error 的两处边界
输出的规则藏在题目里,判题却按字符逐一比对。最常见的失败是PE(Presentation Error),意思是答案数字全对,但空格、换行的位置和标准答案不一致。题段里主要两类:一类要求“每组输出占一行”,这类只需要printf末尾带\n;另一类要求“两组输出之间空一行”,很多人写成每组输出后都打两个\n,结果最后一组后面多了一个空行,被判格式错误。
int flag = 0; /* 记录是否已经输出过一组 */ /* 每算出一组答案后 */ if (flag) { printf("\n"); /* 从第二组开始,先补空行 */ } printf("%d\n", ans); flag = 1;这个“先判断再输出空行”的顺序是关键:第一组之前不打印空行,最后一组之后也不多打印空行,空行只出现在两组答案之间。另一个高频格式问题是行尾空格。题段里有的题目要求“每个数之间用一个空格隔开”,直接用for循环printf("%d ", a[i])会在最后一个数后面多出一个空格,造成PE。正确写法是“先输出第一个数,后续每个数前面补一个空格”:
for (i = 0; i < n; i++) { if (i) printf(" "); /* 从第二个数开始,前面加空格 */ printf("%d", a[i]); } printf("\n");养成“先判断后补位”的习惯,比每次提交后看PE提示再回头改高效得多。这两个模板覆盖了题段里绝大多数输出格式要求,建议直接背下来当默认写法。
3. 题段里的高频题型:A+B 变体、简单数学与初等模拟
3.1 A+B 变体:读入方式比加法本身更值得总结
整个1000-1099题段里,A+B及其变体占的比例最大。变化不在加法本身,而在数据怎么给:可能是每行两个数、可能有T组、可能是每行先给个n再给n个数、可能以0结束、可能一次给了一整行用空格分隔。把读入方式分好类,每类固定一套模板,做题速度立刻不一样。
#include <stdio.h> int main(void) { int n, i, sum, x; while (scanf("%d", &n) != EOF) { if (n == 0) { /* 遇到 0 行终止 */ break; } sum = 0; for (i = 0; i < n; i++) { scanf("%d", &x); sum += x; } printf("%d\n", sum); } return 0; }这个例子同时演示“EOF循环”和“遇0结束”的组合。细节在if (n == 0)的位置:它必须在scanf读完n之后、读取后续n个数之前判断,否则一旦读到0,程序还会继续读本行并不存在的数据。sum定义在每组循环内部,天然避免了上一组数据残留。有人习惯把sum定义在main外面,那每组开始就要记得重新赋零,两类写法选一种固定下来就行。
A+B变体里还有一类需要防溢出的题:比如计算1到n的和、平方和,n给到10^5时答案已经超过int的范围。判断依据很简单,估算一下最坏情况的量级,超过21亿就把int换成long long,printf里对应的格式化符也换成%lld。题段里WA看不到具体原因,很多人查半天逻辑最后发现是类型不够大,这类血泪经验值得记一次。
3.2 gcd 与素数判断:两道背下来就能过一类题的模板
数学题在题段里大致分成三类:最大公约数、素数判断和数列计算。前两类有固定的实现,背下来后遇到直接填空,不需要每次重新推导。
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; /* 递归到 b=0 为止 */ } int is_prime(int n) { int i; if (n < 2) return 0; /* 0 和 1 不是素数 */ for (i = 2; i * i <= n; i++) { if (n % i == 0) return 0; } return 1; }gcd用辗转相除法,参数顺序无所谓,函数内部会自动交换。is_prime的循环条件写成i*i<=n而不是i<=sqrt(n),是为了避免浮点运算和可能的精度误差;n<2的特判别漏,因为题段里可能有n=0或n=1的边界测试。枚举范围缩到根号n,在n为int最大值时循环也就四万多次,单组数据完全够快。
如果题目要求输出某个区间所有素数,逐个调is_prime在题段数据量下通常也没问题。但一旦询问次数多,比如几百次查询,每次都从头枚举就会显得笨重,这时要改用后面第6章说的预处理。现阶段先把单个判断写对,把int和long long用对,数学题就稳了一半。
3.3 初等模拟题:读题列步骤,再动手写代码
模拟题是题段里的分水岭,它不考算法,考的是“把文字规则翻译成代码”的耐心。常见题材是时间日期换算、分数运算、字符串处理。我的习惯是先建一个数据流清单:输入什么格式、中间要算出什么、输出什么格式,然后把计算过程拆成两步以上。以时间差计算为例,统一转成秒再相减,比直接做借位运算简洁得多。
#include <stdio.h> int main(void) { int h1, m1, s1, h2, m2, s2; scanf("%d:%d:%d", &h1, &m1, &s1); scanf("%d:%d:%d", &h2, &m2, &s2); /* 统一换算成秒,避免跨小时的借位错误 */ int t1 = h1 * 3600 + m1 * 60 + s1; int t2 = h2 * 3600 + m2 * 60 + s2; printf("%d\n", t2 - t1); return 0; }时间类模拟的核心是“化整为零”:小时、分钟、秒先统一成一个单位,算完再按需拆回去。如果输出要求“HH:MM:SS”补零格式,就用printf("%02d:%02d:%02d", h, m, s),%02d表示不足两位时补0。字符串类模拟也有类似原则,比如统计每个字母出现次数,用一个长度为26的数组当下标桶,c - 'a'把字符映射到0到25,比switch逐个判断干净得多。
模拟题的另一个关键教训是不要追求一次把边界全想全,而是先按主流程写出来,然后针对几个边界点单独测:时间跨零点、分数为0、字符串为空、输入有前导空格。题段数据里这些边界大量存在,样例通过不等于边界通过。我一般写完主逻辑后,花两分钟把边界值手工列出来跑一遍,提交前心里有底得多。
4. 三套可提交的模板:C、Java、Python 的写法和取舍
4.1 C:scanf/printf 与全局数组是题段默认配置
C在本题段的最大优势是IO简单且速度快,scanf/printf的性能对题段数据量完全够用。写数组题时要养成一个习惯:较大的数组声明在main外面,作为全局变量,而不是定义在main内部。局部大数组会占用栈空间,题段里n可以到十万甚至更大,栈上声明的数组一旦过大,运行错误跑不掉。
#include <stdio.h> #include <stdlib.h> int a[100005]; /* 全局数组,默认初始化为 0,且不占栈空间 */ int cmp(const void *x, const void *y) { int u = *(const int *)x; int v = *(const int *)y; if (u < v) return -1; /* 升序 */ if (u > v) return 1; return 0; } int main(void) { int n, i; while (scanf("%d", &n) != EOF) { for (i = 0; i < n; i++) scanf("%d", &a[i]); qsort(a, n, sizeof(int), cmp); for (i = 0; i < n; i++) { if (i) printf(" "); /* 控制行尾不留空格 */ printf("%d", a[i]); } printf("\n"); } return 0; }这段代码展示了三个常规配置:全局数组、qsort比较函数、行内空格控制。cmp返回-1、0、1而不是直接返回u-v,是因为u-v在极端差值下可能溢出int,产生错误排序。如果用的是C++,直接用algorithm里的sort加lambda或bool函数,代码更短。排序题在题段里常以“输入n个数,排序后输出”出现,记住“先输出判断空格”的写法能直接避开PE。
C语言在这里还有一个好处:格式化输出由程序员完全掌控,保留几位小数、补零、对齐都在printf的格式串里完成,不容易被语言本身的隐式转换坑到。
4.2 Java:Main 类、BufferedReader 与别用 Scanner 偷懒
Java提交时的第一条规则是:类名必须是Main,main方法签名固定为public static void main(String[] args),不能带package语句。很多人在IDE里建工程时自动带上包名,提交时忘了删,直接编译错误,这个错误在题段里出现频率远超想象。一旦遇到编译不过,先看是不是类名和包问题。
IO方面,Scanner写起来方便,但底层逐个解析,数据量稍大就慢。题段的数据规模用Scanner多数能过,但既然要养成长期刷题习惯,不如从一开始就用BufferedReader按行读,付出一点点代码量换稳定。
import java.io.*; public class Main { public static void main(String[] args) throws Exception { BufferedReader br = new BufferedReader( new InputStreamReader(System.in)); String line; while ((line = br.readLine()) != null) { line = line.trim(); if (line.isEmpty()) continue; String[] parts = line.split("\\s+"); int a = Integer.parseInt(parts[0]); int b = Integer.parseInt(parts[1]); System.out.println(a + b); } } }readLine读到文件结尾返回null,正好对应EOF。trim去掉行首行尾空白,split("\s+")按连续空白切分,避免行内多个空格拆出空串。这套读法在处理“每行固定若干数据”的题目时比Scanner快,尤其在数据量大的题段后面部分优势明显。输出用System.out.println自带换行,组间空行用前面讲过的flag控制,逻辑与C一致。
有一个容易翻车的细节:BufferedReader处理的是整行,如果一行里有大量数字且行非常长,String.split会生成很多子串对象,内存略涨。题段里这种情况罕见,先不用过度优化,保持代码可读性更重要。
4.3 Python:sys.stdin 全量读入,赢在 IO 而不是语法
Python在这类入门题段里非常舒服,但要避开一个新手最常见的低效写法:用input()一行一行读,数据量大时单个读入的开销会累积。题段规模下多数能过,不过一旦遇到输入数据较多的题,换全量读入立刻见效。
import sys data = sys.stdin.read().split() # data 是去掉所有空白后的 token 列表 i = 0 while i + 1 < len(data): a = int(data[i]) b = int(data[i + 1]) print(a + b) i += 2sys.stdin.read()把整个输入读成字符串,split()按任意空白切成token列表,程序完全不关心换行和空格的区别。T组数据时,t = int(data[0])后按顺序消费即可,每组取固定数量的token。这个写法比反复调用input()快得多,逻辑也更直白。
Python的print默认在行尾加换行,和OJ的“每组一行”天然匹配。浮点输出用format或者f-string控制小数位,比如print(f"{avg:.2f}")。需要注意Python的int无上限,这让它在处理需要大数的题目时比C、Java更省心,但运算速度慢,题段里如果遇到数据量大的数学题,C仍是第一选择。
| 语言 | 适合题型 | 主要风险 |
|---|---|---|
| C/C++ | 数组、数学、排序、性能敏感题 | 指针、数组越界、类型溢出 |
| Java | 常规题型,工程习惯延续 | 类名、Scanner过慢、对象开销 |
| Python | 输入输出简单、大整数、快速原型 | 循环慢、递归深度限制 |
这张表是我对题段三语言的基本判断。入门阶段建议把C或C++作为主力,不是因为别的,而是题段里的数组和指针操作在C里最直观,排错时能看清内存边界。
5. 避坑笔记:从编译错误到 WA 的五个高发坑
5.1 编译与运行时的三个坑:类名、数组、变量重置
第一个坑:Java提交编译错误,提示找不到Main类。现象是本地IDE运行正常,复制到OJ提交就编译失败。原因大多是类名不是Main,或者文件里带着package语句。解决:把public class后的名字改成Main,删掉package行,保持只有一个public类。
第二个坑:数组越界导致运行错误(RE)。现象是样例通过,提交后报Runtime Error。原因很可能是题目给的最大数据超出你数组开的大小,或者循环上限写的是n而不是n-1,访问了a[n]这个不存在的下标。还有一种隐蔽情况:scanf的返回值被忽略,读入失败后循环还在跑,表现成超时或崩溃。解决:数组一律开成题目数据上限加5,比如最多100000个,就开a[100005],别刚刚好;循环里凡是访问a[i]和a[i+1]的,都要确认i的取值范围不会越界。
第三个坑:多组数据间的变量残留。现象是第二组数据算出的结果比预期大,或者前一组的结果叠加到下一组。原因是没有在每组开始时把sum、cnt等累积变量清空。解决:把这类变量定义在循环体内部,每组自动重新初始化;如果定义在外部,就在每组开头显式赋零。这个问题在循环结构写复杂后特别容易犯,提交前扫一遍所有累积变量,确认它们的作用域。
5.2 格式与精度的两个坑:PE 与 float
第四个坑:Presentation Error。现象是提示PE而不是WA,说明答案数字全对,就是格式不一致。原因集中在三处:行尾多余空格、组间空行位置不对、输出里混入了调试用的打印。解决:行尾空格用“先判断再补位”模板统一处理;组间空行用flag控制;提交前删掉所有printf调试语句。PE虽然不罚重,但频繁PE说明输出习惯还没固定,值得专门花半小时把所有输出模板过一遍。
第五个坑:浮点数精度。现象是用float提交WA,改成double就AC;或者答案差0.01。原因是float只有约7位有效数字,题段里的平均值、比例、浮点比较题会精确卡到边界,float的舍入误差足以让结果在四舍五入时差一位。解决:一律用double,读入用%lf,输出按题目要求格式化。题目要求保留两位小数时用printf("%.2f", ans),让printf自己做四舍五入,不要手工乘100加0.5再取整,那个办法在负数和边界情况下会翻车。
排查顺序也值得固定下来:先看编译错误,再看运行错误,然后拿样例跑一遍,最后把最大的边界数据手工构造一遍再提交。题段里90%的WA都能在这四步里定位,剩下的少数属于题意理解偏差,重读一遍题目描述比盲目改代码有效。提交失败时保留上一次的代码版本,别一边改一边丢,万一新思路越改越远,至少能退回上一版。
6. 从 AC 到跑得更快:预处理、打表和稳定的提交习惯
6.1 提前算好:素数表与前缀和
题段里有的题目会多次询问“某个区间内素数的个数”或“前n项和”。如果每组输入都重新判断一次,会浪费大量重复计算。更稳的做法是:在读取输入之前,先把数据范围内的答案全部算好,再处理每组询问。
#define MAXN 100000 int is_p[MAXN + 1]; int pre[MAXN + 1]; /* pre[i] 表示 1..i 的素数个数 */ void init(void) { int i, j; for (i = 2; i <= MAXN; i++) is_p[i] = 1; for (i = 2; i <= MAXN; i++) { if (is_p[i]) { for (j = i + i; j <= MAXN; j += i) { is_p[j] = 0; /* 素数的倍数都不是素数 */ } } } for (i = 1; i <= MAXN; i++) { pre[i] = pre[i - 1] + is_p[i]; /* 前缀和累计 */ } }预处理的思想是把“高频查询”变成“查表”。is_p数组是一次性算好的素数标记,pre数组让任意区间查询变成pre[r] - pre[l - 1],时间复杂度从每次枚举降到常数。这个套路不仅用于素数,前缀和能推广到区间求和、区间计数一类问题。题段里数据范围不大,但学会这个习惯后,遇到后面更大范围的题不会慌。需要注意数组下标全部加一,pre[0]保持0,避免访问pre[-1]。
6.2 打表提交:本地算完,代码只负责输出
当题目输入范围小、答案数量有限时,打表是比预处理更极端的做法,我一般会先在本地把每个可能输入的答案算好,必要时导出成数组,提交的代码只负责查表输出。
/* 本地计算后生成的答案表,下标对应输入 */ int ans[] = {0, 1, 1, 2, 3, 5, 8, 13, 21}; int main(void) { int n; while (scanf("%d", &n) != EOF) { printf("%d\n", ans[n]); /* 直接查表输出 */ } return 0; }打表适合范围固定、结果可枚举的题,比如输入n不超过20、问某个递推结果。适用范围是答案总数少,且每个答案与输入一一对应;输入范围大到几万时,表就失去意义,回到预处理或正常算法更合理。打表的代价是代码难读,别人看不出你这个表的来源,所以实际应用要克制,别为了短代码牺牲可维护性。
我在这个题段最深的教训是:AC不是终点,提交前花两分钟对照检查单——读入是否覆盖所有输入形式、输出是否用固定模板、数组是否多开了几个位置、累积变量是否重置——比事后罚时重交高效得多。这套习惯从1000-1099题段养成了,后面几千题都一直受益。希望帮到你。
本文还有配套的精品资源,点击获取