ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

NUIST程序设计基础OJ题库:从WA到AC的判题原理与避坑指南

NUIST程序设计基础OJ题库:从WA到AC的判题原理与避坑指南

简介:这份资源是南京信息工程大学(NUIST)程序设计基础课程的OJ题库配套资料,面向正在学习C++或Java编程、准备算法训练与课程实践的学生,也适合希望系统刷题巩固数据结构与算法设计能力的自学者。压缩包为zip格式,整体约482.72MB,文件总数与类型明细上游暂未提供,但结合OJ题库场景,通常包含源代码文件、测试数据及编译运行结果等工作目录内容,便于回顾解题思路与调试过程。题库覆盖排序、二分查找、深度优先与广度优先搜索、最短路径、最小生成树、动态规划等经典算法,并涉及数组、链表、栈、队列、二叉树、哈希表等数据结构,可帮助读者在实时评测反馈中快速定位错误、优化时间与空间复杂度。目前已有751人学习下载,适合作为课程作业练习、竞赛入门与算法查漏补缺的实战素材。

1. 程序设计基础 OJ 题库 NUIST:从“能跑”到“能过”的那道坎

很多同学第一次在 NUIST 的程序设计基础课上打开 OJ 题库,都会经历同一个瞬间:本地 Dev-C++ 里跑得好好的代码,提交上去却是一个红色的 Wrong Answer,或者更让人摸不着头脑的 Runtime Error。这不是玄学,而是 OJ 在线判题系统和本地编译器的运行环境、输入输出规范、边界处理逻辑存在系统性差异。程序设计基础 OJ 题库 NUIST 这套题目的价值,恰恰在于它把 C 语言和 C++ 程序设计里最容易翻车的那些点——多组输入、格式敏感、数组越界、整数溢出——全部压缩进了几十道循序渐进的题目里。它适合刚学完语法、想通过 oj 刷题把知识变成肌肉记忆的初学者,也适合想拿它当教学案例的助教。接下来的内容,我会按“判题原理 → 环境搭建 → 题型拆解 → 避坑排查 → 进阶技巧”的顺序,把从零到稳定 AC 的路径讲清楚。

2. OJ 判题系统到底在背后做了什么:输入输出重定向与比对逻辑

2.1 从点击“提交”到返回结果,中间经历了什么

很多人把 OJ 当成一个黑匣子,觉得 AC 或 WA 是运气。其实主流 OJ 的判题流程非常固定:你的源代码被上传到服务器后,判题机先调用编译器(通常是 gcc 或 g++)把它编译成可执行文件,编译失败直接返回 Compilation Error。编译成功后,判题机把题目预设的输入数据文件通过标准输入(stdin)喂给你的程序,同时把你的程序输出重定向到一个临时文件,最后拿这个临时文件和标准答案文件做逐字节或逐行比对。比对方式决定了你需要注意什么:如果是逐字节比对,行末多一个空格都会 WA;如果是忽略行末空格的比对,容错率稍高,但换行符数量仍然敏感。

NUIST 的程序设计基础题库大多采用标准 ACM 赛制判题逻辑,也就是“读入一组数据、输出一组结果、循环直到输入结束”。这意味着你的程序不能只处理一组输入就退出,也不能在输出里加任何提示性文字,比如printf("请输入n:")这种在本地调试时很舒服的写法,提交上去必错。判题机不会看你的提示语,它只认标准输出里的数字和字符。

另一个容易被忽略的点是时间限制和空间限制。程序设计基础题目的时间限制通常在 1 秒到 2 秒之间,空间限制在 32MB 到 256MB 之间。C 语言里一个int占 4 字节,开一个int a[1000000]就是 4MB,看起来不大,但如果你在函数内部开这么大的数组,栈空间可能直接爆掉,返回 Runtime Error。常见做法是把大数组定义成全局变量,或者用static修饰,让它落在静态存储区而不是栈上。

2.2 本地复现判题环境的最小操作

要减少“本地能跑、提交就挂”的情况,最有效的办法是在本地模拟判题机的输入输出方式。具体操作是:把你的程序编译成可执行文件,然后用文件重定向的方式喂输入、接输出,再和手算结果对比。

# 假设你的源代码文件是 main.c gcc main.c -o main -O2 -Wall # 手动构造一个输入文件 input.txt,内容按题目要求写 # 然后运行程序,把输出重定向到 output.txt ./main < input.txt > output.txt # 查看输出结果 cat output.txt

这段命令里,-O2是开启编译器优化,NUIST 判题机通常也会开优化,本地不开可能导致某些依赖未定义行为的代码在本地“碰巧正确”、提交后出错。-Wall是打开所有警告,很多边界问题编译器早就提醒你了,只是你忽略了警告信息。< input.txt和> output.txt分别代表标准输入重定向和标准输出重定向,这是判题机最核心的动作,本地模拟一遍就能提前发现“多组输入没循环”“输出多打了空格”这类低级错误。

参数方面,-O2不是必须的,但建议加上,因为它能暴露一些优化后才出现的未定义行为。-Wall强烈建议常开,尤其是初学阶段,编译器警告就是你的免费代码审查员。如果你用的是 C++,把gcc换成g++,文件后缀改成.cpp,其余逻辑完全一样。

提示:NUIST 题库里部分题目明确要求“多组输入”,但题目描述可能只在一开始提一句,后面不再重复。看到“输入包含多组测试数据”或“输入直到文件结束”这类字眼,就必须用while(scanf(...) != EOF)或while(cin >> n)的结构。

2.3 输入输出格式的敏感度:为什么多一个空格就 WA

OJ 的比对逻辑决定了输出格式的容错率极低。以一道典型的“输出两个数的和”为例,标准答案文件里写的是3\n,你的程序输出3 \n(数字后面多一个空格),逐字节比对就会失败。更隐蔽的情况是行末换行符:有些题目要求最后一行输出后也有换行,有些题目则对末尾换行不敏感,但你不能赌,最稳妥的做法是严格按照题目样例的格式来。

常见做法是:每道题先在本地把样例输入跑一遍,用cat -A output.txt查看输出的不可见字符。cat -A会把换行显示为$,把制表符显示为^I,这样你就能确认自己的输出和样例是否完全一致。这个习惯能帮你省下大量“明明答案对但就是 WA”的调试时间。

另一个高频翻车点是浮点数输出。题目要求“保留两位小数”,你就必须用printf("%.2f", ans),不能用cout << ans默认输出。C++ 里可以用cout << fixed << setprecision(2) << ans,但需要包含<iomanip>头文件。浮点数的比对通常有精度容差,但输出格式本身仍然是严格的,该保留几位就保留几位,不能多也不能少。

3. NUIST 题库高频题型拆解:从循环到数组的 AC 路径

3.1 多组输入的三种写法与选择依据

NUIST 程序设计基础题库里,多组输入是贯穿始终的考点。根据题目给出的输入终止条件不同,写法分为三类。

第一类:明确告诉你有多少组数据,第一行是组数 T。这种最简单,直接循环 T 次。

#include <stdio.h> int main() { int t; scanf("%d", &t); // 读取组数 while (t--) { // 循环 t 次 int a, b; scanf("%d %d", &a, &b); printf("%d\n", a + b); } return 0; }

这段代码的逻辑是:先读组数t,然后while(t--)会执行t次,每次读一对整数并输出和。参数上,t--是后置自减,先判断t是否为真(非零),再减一,所以循环次数正好是t次。注意输出必须带\n,否则多组结果会挤在一行。

第二类:输入直到文件结束(EOF),没有明确的组数。这是 NUIST 题库里最常见的多组输入形式。

#include <stdio.h> int main() { int a, b; while (scanf("%d %d", &a, &b) != EOF) { // 读到文件结束 printf("%d\n", a + b); } return 0; }

scanf的返回值是成功读入的变量个数,当读到文件末尾时返回EOF(通常是 -1)。while(scanf(...) != EOF)就是“只要能读到两个整数就继续”。更简洁的写法是while(~scanf("%d %d", &a, &b)),利用按位取反,但可读性不如显式写!= EOF,初学阶段建议用后者。

第三类:输入以特定值结束,比如“输入 0 0 表示结束”。这种需要在循环内部判断。

#include <stdio.h> int main() { int a, b; while (scanf("%d %d", &a, &b) == 2) { // 确保读到了两个数 if (a == 0 && b == 0) break; // 遇到 0 0 退出 printf("%d\n", a + b); } return 0; }

这里scanf(...) == 2比!= EOF更严谨,因为如果输入中途出现非数字字符,scanf可能返回 1 或 0,用== 2能确保两个变量都成功赋值。break直接跳出循环,不再处理后续输入。

3.2 数组类题目的边界处理:越界与初始化的血泪经验

数组是程序设计基础里最容易出 Runtime Error 的地方。NUIST 题库里常见的数组题包括“求最大值最小值”“数组逆序”“统计字符出现次数”等。这类题目的核心坑点有三个:数组开多大、下标从几开始、要不要初始化。

以“统计一段字符串中每个字母出现的次数”为例,标准做法是开一个大小为 26 的计数数组,初始化为 0,然后遍历字符串,用count[str[i] - 'a']++累加。

#include <stdio.h> #include <string.h> int main() { char str[1005]; // 开足够大的字符数组 int count[26] = {0}; // 计数数组,必须初始化为 0 scanf("%s", str); // 读入字符串,不含空格 int len = strlen(str); for (int i = 0; i < len; i++) { if (str[i] >= 'a' && str[i] <= 'z') { count[str[i] - 'a']++; // 映射到 0~25 } } for (int i = 0; i < 26; i++) { if (count[i] > 0) { printf("%c: %d\n", 'a' + i, count[i]); } } return 0; }

这段代码里,char str[1005]的大小是根据题目数据范围定的,如果题目说字符串长度不超过 1000,那开 1005 是留了余量。int count[26] = {0}的= {0}不能省,否则数组里是随机值,累加结果完全错误。str[i] - 'a'是把小写字母映射到 0 到 25 的下标,前提是你已经用if判断了字符范围,否则遇到大写字母或数字会越界访问count数组,直接 Runtime Error 或 WA。

另一个高频错误是字符串输入用scanf("%s", str)时,遇到空格会停止读取。如果题目输入的字符串包含空格,必须用fgets(str, sizeof(str), stdin)或gets(不推荐,已废弃)。fgets会把换行符也读进去,需要手动处理掉末尾的\n。

注意:NUIST 题库里有些题目会给出“数据范围”提示,比如“1 ≤ n ≤ 1000”。这个范围就是你开数组的依据,不要凭感觉开。开小了越界,开大了浪费空间,极端情况下可能超出空间限制。

3.3 循环与条件判断的常见逻辑陷阱

循环和条件判断本身不难,但在 OJ 环境下,几个细节会反复坑人。第一个是for循环的边界:for (int i = 0; i < n; i++)和for (int i = 1; i <= n; i++)都能遍历 n 个元素,但如果你在循环体内同时用了i作为数组下标和计数变量,混用两种风格就会出错。建议统一用0起始的下标风格,和 C 语言数组的默认下标一致。

第二个是while循环里的变量更新。比如“求一个数的各位数字之和”,常见写法是while (n > 0) { sum += n % 10; n /= 10; }。如果漏掉n /= 10,就是死循环,判题机会返回 Time Limit Exceeded。死循环在本地跑的时候可能因为输入数据小、你手动中断了而没发现,提交后必 TLE。

第三个是if-else链的顺序。比如“判断成绩等级”,如果先判断score >= 60输出“及格”,再判断score >= 90输出“优秀”,那 90 分以上的成绩会先命中“及格”分支,永远到不了“优秀”。正确的顺序是从高到低判断,或者用else if把条件写完整。这类逻辑错误在本地用样例测试时可能刚好没覆盖到边界值,提交后遇到边界数据就 WA。

4. 避坑与排查:NUIST OJ 提交后不 AC 的 5 个真实原因

4.1 现象:本地输出和样例一模一样,提交却是 Wrong Answer

原因通常不是答案算错了,而是输出格式有不可见差异。最常见的是行末多了空格、少了换行,或者多组数据之间多输出了空行。NUIST 题库的比对逻辑对格式敏感,样例里数字之间是空格,你就不能用制表符;样例最后一行有换行,你就不能省。

解决方法是把样例输入保存成input.txt,运行./main < input.txt > output.txt,然后用cat -A output.txt查看不可见字符,和题目样例的cat -A结果逐行对比。另一个办法是用diff命令:把标准答案保存成answer.txt,运行diff output.txt answer.txt,有差异会直接标出来。

4.2 现象:程序在本地跑得好好的,提交后 Runtime Error

原因大概率是数组越界或除以零。NUIST 题库里数组题的数据范围有时会在题目描述的最后一行给出,容易被忽略。比如题目说“字符串长度不超过 100”,你开了char str[100],但字符串末尾还需要一个\0,实际需要 101 个字节,开 100 就越界了。常见做法是在题目给定范围的基础上加 5 到 10 的余量。

除以零的情况常出现在“求平均值”类题目里,如果输入数据可能为 0 个元素,你的sum / n就会触发除零错误。提交前检查所有除法运算的分母是否可能为 0,必要时加if判断。

4.3 现象:多组输入的题目只输出了一组结果

原因是循环结构写错了。有人用if代替while,有人写了while但循环体内没有重新读取输入,导致死循环或只处理一次。还有一种隐蔽情况:用了scanf但没检查返回值,输入读到 EOF 后a和b保持上一次的值,程序继续用旧值计算并输出,多出一组错误结果。

解决方法是严格按照题目给出的终止条件选择循环写法。不确定的时候,把三种多组输入模板都试一遍,看哪种能过。NUIST 题库的题目描述里通常会有“输入包含多组测试数据”或“输入以 EOF 结束”的明确提示,仔细读题就能确定。

4.4 现象:整数运算结果不对,但小数据测试正常

原因是整数溢出。C 语言里int通常是 32 位,能表示的范围大约是 -21 亿到 21 亿。如果题目涉及阶乘、斐波那契数列、大数加法,中间结果很容易超过这个范围。比如计算 20 的阶乘,结果约 2.4×10^18,远超int范围,甚至超过long long的范围(约 9.2×10^18)。

解决方法是根据题目数据范围选择合适的数据类型。long long能覆盖大多数基础题目的需求,输出时用printf("%lld", ans)。如果题目明确要求处理超大数(比如 100 的阶乘),就需要用数组模拟大数运算,这是 NUIST 题库里少数几道拔高题的内容。

4.5 现象:编译错误提示看不懂,反复提交都 CE

原因可能是用了不支持的编译器特性,或者头文件没包含。NUIST 题库的 C 语言编译器通常是 gcc,C++ 是 g++,标准版本可能是 C99 或 C11。如果你在 C 语言里写了//注释(C99 支持,但极老编译器可能不支持),或者用了for (int i = 0; ...)这种在循环内声明变量的写法(C99 支持),一般没问题。但如果你用了 C++ 的cin/cout却提交到 C 语言题目,就会 CE。

解决方法是先看编译错误的第一条,通常第一条错误会引发后续一连串错误,修好第一条,后面的可能自动消失。常见 CE 原因包括:漏分号、括号不匹配、变量名拼写错误、用了未声明的函数。把本地编译器的警告级别开到-Wall,大部分问题在本地就能发现。

5. 从 AC 到稳定 AC:NUIST 题库的进阶刷题习惯

5.1 用对拍脚本验证边界数据

当你觉得自己的代码逻辑没问题,但提交后就是 WA,而样例又太弱测不出问题时,对拍是最有效的排查手段。对拍的核心思路是:写一个暴力解法(保证正确但可能超时),再写一个随机数据生成器,让两个程序跑同一组随机数据,比较输出是否一致。

# 假设你有一个暴力解法 brute.c 和你的解法 main.c gcc brute.c -o brute -O2 gcc main.c -o main -O2 # 写一个数据生成器 gen.c,编译成 gen gcc gen.c -o gen -O2 # 循环对拍 1000 次 for i in $(seq 1 1000); do ./gen > input.txt # 生成随机输入 ./brute < input.txt > brute_out.txt ./main < input.txt > main_out.txt if ! diff -q brute_out.txt main_out.txt > /dev/null; then echo "差异出现在第 $i 组数据:" cat input.txt break fi done

这段脚本的逻辑是:每次生成一组随机输入,分别喂给暴力解和你的解,用diff -q静默比较两个输出文件。如果不同,就打印出导致差异的输入数据,你拿着这组数据去调试,比盲目改代码高效得多。参数上,seq 1 1000是生成 1 到 1000 的序列,对拍次数根据题目复杂度调整,简单题 100 次就够,复杂题可以到 10000 次。

数据生成器gen.c需要根据题目输入格式来写,用rand()生成随机数,注意控制数据范围覆盖边界值,比如最小值、最大值、特殊值(0、负数)。对拍脚本在 Linux 和 macOS 的终端里可以直接跑,Windows 下用 Git Bash 或 WSL 也能运行。

5.2 建立自己的题型索引与代码模板库

刷 NUIST 题库到一定数量后,你会发现题目类型是有限的:多组输入求和、数组最值、字符串统计、简单排序、素数判断、进制转换。每类题目都有固定的代码骨架,把这些骨架整理成模板,下次遇到同类题目直接套用,能大幅减少调试时间。

我自己的习惯是建一个templates目录,里面按题型存.c文件,每个文件只保留核心逻辑,变量名用通用写法。比如multi_input_sum.c存多组输入求和的模板,array_max_min.c存数组最值的模板。每次遇到新题目,先判断属于哪类,复制模板改逻辑,而不是从空白文件开始写。这个习惯让我在 NUIST 题库上的 AC 率从“看运气”变成了“基本稳定”。

另一个习惯是每道 AC 的题目都保留一份带注释的代码,注释里写清楚这道题的坑点是什么、用了什么技巧。过一段时间回头看,这些注释就是最好的复习材料。程序设计基础的题目不难,但坑点密集,把踩过的坑记下来,比反复刷同类型的题更有价值。

5.3 时间限制与算法选择的平衡

NUIST 题库里大部分题目用暴力循环就能过,但少数题目会卡时间复杂度。比如“判断一个数是否为素数”,如果对每个数都从 2 循环到 n-1,当 n 接近 10^9 时必然超时。常见做法是只循环到 sqrt(n),因为如果 n 有大于 sqrt(n) 的因子,必然有小于 sqrt(n) 的对应因子。

#include <stdio.h> #include <math.h> int is_prime(int n) { if (n < 2) return 0; // 0 和 1 不是素数 for (int i = 2; i <= (int)sqrt(n); i++) { if (n % i == 0) return 0; // 找到因子,不是素数 } return 1; }

这段代码里,sqrt(n)是数学库函数,需要包含<math.h>,编译时可能需要加-lm链接数学库。(int)sqrt(n)把浮点结果转成整数,避免浮点比较的精度问题。循环上界用i <= (int)sqrt(n)而不是i * i <= n,后者在i很大时可能溢出,前者更安全。

对于更复杂的题目,比如“求第 n 个斐波那契数模 10007”,如果 n 达到 10^9,就需要用矩阵快速幂,这是程序设计基础课程之外的进阶内容。NUIST 题库里这类题目通常会有额外提示,比如“本题需要 O(log n) 算法”,看到这种提示就要意识到暴力解法过不了,需要换思路。

刷题到后期,你会逐渐形成一种直觉:看到题目数据范围就能判断该用什么复杂度的算法。数据范围在 10^3 以内,O(n^2) 通常安全;10^5 以内,需要 O(n log n);10^9 以上,必须 O(log n) 或 O(1)。这个直觉不是天生的,是踩了足够多次 TLE 之后长出来的。希望这些经验能帮你在 NUIST 的程序设计基础 OJ 题库上少走几个弯路,把时间花在真正理解算法上,而不是和判题机斗智斗勇。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进