刚接触OJ的人,十有八九都被“明明本地运行没问题,提交却WA”折腾过。尤其是杭电OJ这类在线评测系统,输入输出格式差一点就是红叉一片,连题号靠前的入门题都能卡到怀疑人生。今天这篇就把2000~2011,外加2039这13道题整体拆一遍,按题型归类讲思路、给代码、点坑,重点说明这些题到底在训练什么,以及做完之后你的编程习惯会发生哪些改变。适合刚起步刷题的新手,也适合带新人的朋友当作一份现成的排查手册。
这批题虽然名字叫“入门”,但入门和“随意写”是两回事。你会发现它们考察的几乎全是基本功:读入方式、循环边界、公式精度、输出空格。把这些题吃透,后面遇到更大号的题才会走得顺。
1. 为什么是2000~2011和2039:这批题在入门路线上扮演什么角色
1.1 题号背后其实是一个按知识点排好的梯度
很多在线题库都有“新手村”区间,杭电OJ的2000往后就是典型。题号连续,难度缓慢爬坡,每一道题只新增一个知识点的挑战,前面的题又在为后面的题做铺垫。2039单独看是个几何判断题,放在这里其实是补上了“分支逻辑 + 复杂条件组合”这一环。
我把这13道题的考点、输出要求、以及新手最常见的翻车点整理成了下面这张表,方便你对照自查:
| 题号 | 核心考点 | 输出要求 | 常见翻车点 |
|---|---|---|---|
| 2000 | 字符排序、输入缓冲 | 排序后输出字符 | scanf读%c被换行坑 |
| 2001 | 两点间距离公式 | 保留两位小数 | float精度不稳 |
| 2002 | 球体体积公式 | 保留三位小数 | 4/3整数除法坏结果 |
| 2003 | 实数绝对值 | 保留两位小数 | 只按整数写if |
| 2004 | 成绩等级划分 | 非法成绩报错 | 边界值等于号漏写 |
| 2005 | 日期转天数 | 精确输出整数 | 闰年判断残缺 |
| 2006 | 奇数乘积 | 整数输出 | ans初始化位置不对 |
| 2007 | 区间内偶平方奇立方 | 两个整数输出 | 输入大小顺序没保证 |
| 2008 | 正负零统计 | 三个数字输出 | 没处理n=0结束 |
| 2009 | 数列递推求和 | 保留两位小数 | 当前项没有更新 |
| 2010 | 水仙花数 | 空格分隔输出 | 末尾多空格 |
| 2011 | 交替符号求和 | 保留两位小数 | 1/j写成整数除法 |
| 2039 | 三角形三边判定 | YES/NO | 只判断一条边之和 |
1.2 这几道题真正在训练的三件事
第一件事是多组输入模板。从2000到2011,几乎每道题都是“输入多行数据,直到文件结束”。你必须在开头就养成写while (scanf(...) != EOF)的习惯,而不是只处理一行就结束。
第二件事是输出格式绝对合规。OJ判题是按字符逐字比较的,多一个空格、少一个换行都是Wrong Answer。2000让你体会“字符之间必须有空格,但行尾不能有”这种别扭要求,2010水仙花数又把这个要求强化了一遍。
第三件事是把数学公式翻译成代码的耐心。2001、2002、2009、2011这四道题,本质上都是数学题,翻译过程里藏着整数除法、浮点精度、递推更新这些细节。能不能一次写对,反映的是你有没有在落笔之前把每一步想清楚。
2. 2000字符排序:一道题教会你处理scanf的输入缓冲
2.1 题目本质与最容易踩的坑
2000的题目很简单:输入三组字符,每组三个,按ASCII码从小到大输出。难的地方不在排序,而在读字符。
新手第一版通常会这么写:
scanf("%c%c%c", &a, &b, &c);第一次循环运行正常,第二次循环开始就莫名其妙多读进来一个字符,排序结果全乱。原因是第一次输入完三个字符后敲下的回车键还留在输入缓冲里,第二次%c直接把这个换行符读走了。
正确做法是在%c前面加一个空格,让scanf自动跳过空白字符:
scanf(" %c%c%c", &a, &b, &c);这个空格就是整道题的关键。
2.2 AC代码与交换逻辑
排序逻辑用三次比较加交换就够了,不需要排序算法:
#include <stdio.h> int main() { char a, b, c, tmp; while (scanf(" %c%c%c", &a, &b, &c) != EOF) { if (a > b) { tmp = a; a = b; b = tmp; } if (a > c) { tmp = a; a = c; c = tmp; } if (b > c) { tmp = b; b = c; c = tmp; } printf("%c %c %c\n", a, b, c); } return 0; }第一次比较把最小的放到a,第二次再让a和c比,最后处理b和c。三轮下来保证 a <= b <= c。
2.3 经验
我在带新人的时候发现,很多人2000这道题卡住之后,会把问题归结为“排序写错了”,其实是输入缓冲没搞定。这个坑不解决,后面凡是涉及字符读入的题都会反复犯。记住一个口诀:读字符,先想空白;不放心,就在%c前加空格。2000是第一次用血的教训让你理解输入缓冲这个概念,所以它才被放在这个位置。
3. 2001、2002、2003:数学公式题的精度与除法陷阱
3.1 2001两点距离:float不够,double才稳
2001输入四个实数坐标,输出两点间距离,保留两位小数。公式是sqrt((x1-x2)^2 + (y1-y2)^2),看起来没有任何难度。
新手常见的错误是用float存坐标。float的尾数精度大约是7位有效数字,坐标一出现小数,平方再开方之后,误差就可能传导到小数点后第二位,恰好和标准答案差一分钱。
#include <stdio.h> #include <math.h> int main() { double x1, y1, x2, y2; while (scanf("%lf%lf%lf%lf", &x1, &y1, &x2, &y2) != EOF) { double dx = x1 - x2; double dy = y1 - y2; double dist = sqrt(dx * dx + dy * dy); printf("%.2lf\n", dist); } return 0; }写这段代码时注意scanf里double类型必须用%lf,输出用%.2lf。sqrt函数在math.h里,顺手把平方拆成变量,是为了让计算过程更清楚,也方便调试。
3.2 2002球体积:4/3和π的精度
2002让你根据球半径输出体积,保留三位小数。公式V = 4/3 * π * r^3,里面藏着一个C语言经典陷阱:4/3在整数除法里等于1。
如果你写成4/3 * PI * r * r * r,由于乘法和除法从左往右算,4/3先被计算成1,整个结果瞬间缩水。必须写4.0 / 3.0:
#include <stdio.h> #define PI 3.1415927 int main() { double r; while (scanf("%lf", &r) != EOF) { double v = 4.0 / 3.0 * PI * r * r * r; printf("%.3lf\n", v); } return 0; }顺便说一句,r * r * r可以直接乘,也可以用pow(r, 3),但简单乘法效率更高,而且这里不会因为精度问题出错。π取3.1415927完全满足三位小数的要求,不用背更多的位数。
3.3 2003绝对值:别把“实数”读成整数
2003要求输出一个实数的绝对值。题目里明确说了是实数,就一定有小数。如果你用int读入,带小数的输入会被截断成整数,绝对值算对了也是错的。
#include <stdio.h> int main() { double x; while (scanf("%lf", &x) != EOF) { if (x < 0) x = -x; printf("%.2lf\n", x); } return 0; }这里还有一个细节:有些题目里说“实数”,但数据范围里全是整数,很多人就放松警惕用int。真正考试或比赛时,数据范围描述里说实数就按实数处理,不要赌它全是整数。
3.4 这三道题的通法
数学公式题有一个标准流程:先在纸上把公式推一遍,确定中间量的类型,再确认输出保留几位小数。2001~2003三连题,就是在反复训练这个流程。看到小数先想double,看到除法先想有没有整数陷阱,看到输出格式先想%lf还是%d。
4. 2004~2008:分支、日期、循环中的边界思维
4.1 2004成绩转换:边界值等于号别漏
2004要求把百分制成绩转成等级:90~100是A,80~89是B,70~79是C,60~69是D,0~59是E,非法的直接输出Score is error!。
用if-else写没什么问题,但边界判断很容易漏等号:
#include <stdio.h> int main() { int score; while (scanf("%d", &score) != EOF) { if (score >= 90 && score <= 100) printf("A\n"); else if (score >= 80 && score < 90) printf("B\n"); else if (score >= 70 && score < 80) printf("C\n"); else if (score >= 60 && score < 70) printf("D\n"); else if (score >= 0 && score < 60) printf("E\n"); else printf("Score is error!\n"); } return 0; }我見過有人写成score > 90代表A,结果90分落到了B,正好踩中边界。这种错误不是不会做,而是写完没有做边界测试。以后凡是带区间的判断,先写边界值走一遍。
4.2 2005第几天:闰年判断和预置表
2005输入形如1985/1/20的日期,输出这一天是当年的第几天。难点一是用scanf("%d/%d/%d")的格式去匹配斜杠,难点二是闰年判断。
比较自然的做法是先定一个每月天数的表,再累加:
#include <stdio.h> int main() { int y, m, d; int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; while (scanf("%d/%d/%d", &y, &m, &d) != EOF) { int sum = d; for (int i = 1; i < m; i++) sum += days[i]; if (m > 2 && ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0)) sum++; printf("%d\n", sum); } return 0; }闰年的判断条件是能被4整除但不能被100整除,或者能被400整除。注意闰年只影响2月之后的天数,所以m > 2才加一。数组第一个元素放0,是为了让days[1]对应1月,写起来不绕。
这道题真正让你养成的习惯是把经常查表的数据提前准备好,而不是在循环里用switch一个个列。
4.3 2006奇数乘积:循环内初始化
2006输入一个n,再输入n个整数,把所有奇数的乘积输出。
#include <stdio.h> int main() { int n, x, ans; while (scanf("%d", &n) != EOF) { ans = 1; for (int i = 0; i < n; i++) { scanf("%d", &x); if (x % 2) ans *= x; } printf("%d\n", ans); } return 0; }如果把ans = 1写在while外面,第一次正确,第二次就开始把上一次的结果乘进去,全错。这种“循环内变量初始化位置”的错误,在统计、求和、乘积类题目里极其常见。你可以记一条规则:输入数据是分组的,跟每一组相关的累计变量,就放在组内初始化。
4.4 2007平方和立方和:先排序再遍历
2007输入两个整数x和y,求x到y之间所有偶数的平方和、所有奇数的立方和。题目没说x一定小于y,所以第一步要交换,保证区间正确。
#include <stdio.h> int main() { int x, y; while (scanf("%d%d", &x, &y) != EOF) { if (x > y) { int t = x; x = y; y = t; } long long oddSum = 0, evenSum = 0; for (int i = x; i <= y; i++) { if (i % 2 == 0) evenSum += (long long)i * i; else oddSum += (long long)i * i * i; } printf("%lld %lld\n", evenSum, oddSum); } return 0; }这里我用了long long,因为当区间范围大时,平方、立方的结果可能超过int范围。很多人做这题时忽略了数据类型,结果样例通过,提交却WA。把int提升到long long不需要成本,但能避免一个隐形的溢出雷。
输出格式要看清题目要求:先偶数的平方和,再奇数的立方和,中间空格,别写反。
4.5 2008正负零统计:n=0结束与浮点读入
2008的输入规则比较特殊:每行第一个整数n表示后面有n个数,n为0时结束。数本身可能是正数、负数、零,也可能是小数。
#include <stdio.h> int main() { int n; while (scanf("%d", &n) && n != 0) { double x; int neg = 0, zero = 0, pos = 0; for (int i = 0; i < n; i++) { scanf("%lf", &x); if (x < 0) neg++; else if (x == 0) zero++; else pos++; } printf("%d %d %d\n", neg, zero, pos); } return 0; }这个循环条件scanf("%d", &n) && n != 0很经典:既能判断文件是否结束,又能利用输入值本身作为终止条件。浮点数判断x == 0理论上存在误差问题,但OJ给出的数据是可控的,这里不会翻车。真正要注意的是n=0这组数据不能进入循环体,也不能输出结果。
5. 2009~2011:递推、拆分与交替符号的三道经典小题
5.1 2009数列的和:递推更新而不是重新算
2009的题意大致是:输入一个整数n,和一个正整数m,求数列n, sqrt(n), sqrt(sqrt(n)), ...的前m项和。
#include <stdio.h> #include <math.h> int main() { double n; int m; while (scanf("%lf%d", &n, &m) != EOF) { double sum = 0; double cur = n; for (int i = 0; i < m; i++) { sum += cur; cur = sqrt(cur); } printf("%.2lf\n", sum); } return 0; }核心是cur = sqrt(cur)这一步。它是递推而不是重新计算,每次把当前项开方得到下一项。如果把sum直接对n做m次sqrt然后累加,顺序就乱了。这类递推题目考验的是你能否把一个数列的“生成方式”翻译成循环语句,而不是套公式。
5.2 2010水仙花数:输出格式是关键
2010输入两个整数m和n,输出m到n范围内的所有水仙花数,如果没有则输出no。题目给出的水仙花数是三位数,即各位数字的立方和等于它本身,比如153。
#include <stdio.h> int main() { int m, n; while (scanf("%d%d", &m, &n) != EOF) { int first = 1; for (int i = m; i <= n; i++) { int a = i / 100; int b = i / 10 % 10; int c = i % 10; if (a * a * a + b * b * b + c * c * c == i) { if (!first) printf(" "); printf("%d", i); first = 0; } } if (first) printf("no"); printf("\n"); } return 0; }这里有两个关键点。第一个是数位拆分:i/100得到百位,i/10%10得到十位,i%10得到个位。第二个是空格控制:题目要求“每个水仙花数之间用一个空格隔开”,末尾不能有空格。我通过first标志位实现:第一个数字前不打空格,之后每个数字前都打一个空格,这样中间有空格、末尾没有。
如果直接把每个结果后面跟一个空格,最后一个数字后面会多空格,OJ会判Presentation Error。这种细节就是2000埋下的伏笔,到2010要真正掌握。
5.3 2011多项式求和:1.0/j这种细节决定成败
2011是经典的交错级数求和:1 - 1/2 + 1/3 - 1/4 + 1/5 - ...,输入m组数据,每组一个n,输出前n项和。
#include <stdio.h> int main() { int m, n; while (scanf("%d", &m) != EOF) { for (int i = 0; i < m; i++) { scanf("%d", &n); double sum = 0; int sign = 1; for (int j = 1; j <= n; j++) { sum += sign * 1.0 / j; sign = -sign; } printf("%.2lf\n", sum); } } return 0; }最坑的一行就是sign * 1.0 / j。如果你写成1 / j,在C语言里两个整数相除,1/2直接等于0,整个求和全废。所以必须保证至少有一个操作数是浮点数,写1.0 / j或者(double)1 / j都可以。符号交替用sign = -sign每次取反,比用if (j % 2 == 0)去判断更简洁,也不容易错。
要注意这里输入格式是:第一行m,后面有m行n。所以外层while处理到文件结束,内层用m控制循环次数。如果你只写一次scanf("%d", &n),那就只处理了一组数据,必然超时或WA。
6. 2039三角形:简单题怎么还会有人卡住
6.1 题目描述中的隐藏条件
2039看起来比前面几道还简单:输入m组测试数据,每组三个边长,判断能否组成三角形,输出YES或NO。但它的AC率一直不算高,原因在几个隐藏细节上。
第一,三条边都必须是正数。题目数据范围保证了这一点,但你的判断条件不要额外写a > 0这种冗余逻辑,除非你担心非法输入。第二,任意两边之和要大于第三边,是严格大于,不是大于等于。等于的情况构成退化三角形,在这个题里不算。
6.2 三个典型错误写法
我见过三种最常见的错误版本:
第一种,只判断a + b > c,忘了a + c > b和b + c > a。有些人以为排序后只需要判断最大边和另外两条边的关系,但题目并没有保证输入的边长是按顺序给的。
第二种,写成a + b >= c,把等于的情况也算进去了。样例数据可能不覆盖这种边界,判题数据里一定会有一组。
第三种,用int存边长。如果题目给出的边长可能是小数,int直接截断小数部分;就算全是整数,极端情况下a + b也可能溢出int范围,导致判断完全错误。
6.3 正确打开方式
最稳妥的写法是三边都用double读入,然后三个不等式同时成立:
#include <stdio.h> int main() { int m; scanf("%d", &m); while (m--) { double a, b, c; scanf("%lf%lf%lf", &a, &b, &c); if (a + b > c && a + c > b && b + c > a) printf("YES\n"); else printf("NO\n"); } return 0; }这个写法不依赖排序,思考压力最小。如果你更喜欢先排序再判断,也完全可以,但一定要保证三个不等式都覆盖到。double读入的好处是能够同时处理整数和小数边长,也避免了int溢出问题。
2039放在这批题里,是对前面分支逻辑的一个综合检验:多组输入、浮点读入、多个条件的组合判断。它用最少的代码量考察了你是否真的理解了“条件的严格性”。
7. 刷完这批题,我用什么标准检查自己是不是真的“会了”
7.1 三个自查点
这13道题做完之后,不要急着往前冲。我建议你用三个问题自检一下。
第一个问题:能不能不查资料直接写出多组输入模板?不管是while (scanf(...) != EOF)还是while (scanf(...) && 条件),你要能根据题目形态立刻选对。选不对,后面很多题都会卡在读入阶段。
第二个问题:看到“空格分隔输出”四个字,第一反应是否在控制末尾空格?2000和2010都考了这个点,2039虽然不考,但类似的坑会在后面大量出现。不要每次提交WA了才开始找空格,而是写代码前就想好。
第三个问题:浮点数题目,第一反应是否选double?2001、2002、2003、2009、2011,五道题都在向你强调浮点精度。比赛里因为float丢精度的题,远比因为你算法不熟而错的题多。
7.2 下一步的路线建议
这批题通过后,可以继续往后面的固定区间刷,每天四到六道,不贪多。刷题时别只满足于“AC了”,每AC一道都想想:这题用到了哪个知识点,我有没有更优的写法,能不能从别人代码里学到更简洁的表达。
尤其建议你把2000~2011+2039当作一组“体检题”。第一次刷的时候记录每道题出现过的错误类型,过一个月再重新刷一遍,看同样题目能不能一遍AC。多数情况下,第二次的WA次数会明显下降,因为真正发生变化的不是你对某道题的熟悉程度,而是你对输入输出、边界条件、数据类型这三个维度的敏感度,这才是这批题真正想给你的东西。