做OJ刷题的人,十有八九会对鸡兔同笼问题印象很深。我第一次在在线评测系统上做到OJ1004这道题时还觉得挺意外:题目描述就一句话,笼子里关着鸡和兔,数头有n个,数脚有m只,问各几只。这不就是小学奥数题吗?可真正以OJ题目的形式提交代码以后才发现,数学上三秒得出答案,不代表编码一次能通过。本文就借这道“相当于鸡兔同笼问题”的OJ1004,把问题背后的数学模型、输入输出规范、边界条件处理、提交报错排查这些事完整捋一遍。适合刚接触OJ平台、还不熟悉判题规则的新手,也适合准备各类在线笔试的求职者。
1. 内容整体设计与思路拆解
1.1 为什么说它“相当于鸡兔同笼”
很多OJ题不会老老实实把“鸡兔同笼”四个字写在标题里,而是换个外壳,比如停车场里的自行车和汽车、笼子里的小鸡和小猪,但拆开一看全是同一个二元一次方程组。以最原始的鸡兔同笼来说,设鸡有x只,兔子有y只,头的总数是h,脚的总数是f,那么可以列出两个方程:
- 头的数量:x + y = h
- 脚的数量:2x + 4y = f
联立解方程组,用第二个式子减去两倍的第一个式子:
2x + 4y - 2(x + y) = f - 2h
左边化简之后就是2y,于是:
y = (f - 2h) / 2
也就是说,兔子的数量等于“多出来的脚数的一半”。换一个更生活的说法:假如笼子里全都是鸡,那脚数应该是2h只,现在实际有f只脚,多出来的每一只脚都说明有一根“兔腿”没有被算对,而每只兔子比鸡多2只脚,所以兔子数就是(f-2h)/2。这个思路比死记公式好记得多,遇到系数变化也更容易套。
但问题是,x和y是真实世界的数量,必须是整数且不能为负数,所以题目真正考察的是:能不能把这些隐藏约束转化成代码里一个不漏的判断条件。大多数提交踩坑都踩在这个地方。
1.2 这道题在OJ题库里的定位
OJ1004这种编号常见于教学型的在线评测系统,从编号就能看出来它属于“入门期必做”的题目。出题人把它安排在前1000多题的区间,目的很明确:让新手用最简单的题目熟悉OJ平台的完整流程——读题、设计输入输出、本地运行、提交代码、查看判题测试报告。很多人在这里就开始踩坑,比如不知道要处理多组输入、不知道输出格式不能多一个空格、不知道类名必须写成Main。
从算法角度来说,这道题没有任何复杂度可言,时间复杂度是O(1)、空间复杂度也是O(1)。但换个角度看,它又是最考验基本功的题,因为它逼着你理解平台规则。会写for循环不意味着你能在OJ平台上通过这道题,这句话我见过太多反例了。
1.3 不同OJ平台的同类题目差异
我最初是在课程OJ上做的,后来又陆续在华为OJ、西科大OJ平台、东华OJ上见过类似的变体。别看题目表面差不多,平台的输入输出要求差异可能很大,这里做一个对比:
| 平台 | 输入风格 | 输出格式 | 注意事项 |
|---|---|---|---|
| 华为OJ | 常见单组输入,用空格分隔 | 一行输出,不能带无关注释 | 隐藏用例较多,特判严格 |
| 西科大OJ平台 | 部分题目为多组输入直到EOF | 每组结果换行 | 需要处理多行输入 |
| 东华OJ | 常见多组数据,以特定标记结束 | 结果之间留空行 | 注意题目里的结束条件 |
| 其他通用题库 | 单组和多组都有 | 大小写、下划线敏感 | 一切以题目描述为准 |
同样的解题逻辑,换一个平台,可能就因为“是否循环读入”而判成WA。这也是我建议每次提交前都重新看一遍输入输出描述的原因,不能看到“相当于鸡兔同笼”就直接把以前某个平台的代码原封不动贴过去。
2. 核心细节解析与实操要点
2.1 判题系统的判定逻辑
OJ判题系统的工作方式,简单来说就是你提交源代码,服务端用事先准备好的测试用例把你的代码编译、运行,然后把程序输出和标准答案逐字符比对。比对的字母缩写大家都应该眼熟:
- AC(Accepted):恭喜,完全通过
- WA(Wrong Answer):答案错误
- TLE(Time Limit Exceeded):超时
- RE(Runtime Error):运行时错误
- MLE(Memory Limit Exceeded):内存超限
- PE(Presentation Error):输出格式错误
对OJ1004这种简单题来说,最常见的卡壳是WA和PE。WA多半是边界条件或数学公式出错,PE则往往是多打印了一个空格、少换了一行。很多教程只教你写代码,不教你解读判题测试报告,但报告其实是很好的反馈。比如它提示你在“脚数为奇数”这个用例上失败,基本可以确定是忘了判断(f-2h)的奇偶性。
2.2 输入输出格式的坑
先说输入。鸡兔同笼题目常见的输入方式有三种:
- 单组输入,一行两个整数
- 多组输入,读到EOF结束
- 多组输入,以特定终止标记结束,比如0 0
我见过很多新手写的代码是这样:
scanf("%d%d", &h, &f); printf("%d %d", chicken, rabbit);如果是单组测试题,这没问题。但如果题目写的是“多组测试数据”,你只读一组,判题系统拿后面几组用例去跑,你的程序直接结束了,结果必然是WA。处理多组输入的标准姿势是:
- C/C++:
while (scanf("%d %d", &h, &f) != EOF) - Java:
while (in.hasNextInt()) - Python:用
sys.stdin逐行读取
再说输出。OJ的输出比较是逐字符进行的,多一个空格、少一个换行都可能判错。鸡兔同笼的答案一般是先输出鸡的数量,再输出兔的数量,中间一个空格,最后换行。如果题目要求无解时输出“No answer”,那大小写和空格也要和题目描述完全一致,写成了“No Answer”照样WA。
2.3 边界条件的合法性判断
这题的边界条件,概括起来就是一个不等式组加上一个奇偶性判断。设头数为h、脚数为f:
- 脚数不能少于所有动物都只有2条腿的情况,所以f必须大于等于2h
- 脚数不能多于所有动物都有4条腿的情况,所以f必须小于等于4h
- 因为鸡有2条腿,所以(f-2h)必须是偶数,否则兔子数量不是整数
- 上面三个条件都满足时,兔子数(f-2h)/2自然非负,鸡数h-兔子也自然非负
举个例子:头3个,脚7只。按公式硬算,兔子=(7-6)/2=0.5,不是整数,这组数据实际上不可能存在。如果代码里不做取模判断,直接当整数输出,就会得到错误结果。所以核心判断可以写成:
if f < 2 * h or f > 4 * h or (f - 2 * h) % 2 != 0: print("No answer")注意:有些题目会明确写“测试数据保证有解”,这种情况下你可以省略合法性判断直接算。但保险起见,多写一层判断通常不会影响正确性,也让你在遇到隐藏用例时更稳。
3. 实操过程与核心环节实现
3.1 C语言实现
C是OJ上最常见、也最能暴露细节的语言。完整代码可以这样写:
#include <stdio.h> int main() { int h, f; while (scanf("%d %d", &h, &f) != EOF) { int diff = f - 2 * h; if (f < 2 * h || f > 4 * h || diff % 2 != 0) { printf("No answer\n"); continue; } int rabbit = diff / 2; int chicken = h - rabbit; printf("%d %d\n", chicken, rabbit); } return 0; }这里有两个细节值得说。第一,while(scanf(...) != EOF)可以同时兼容单组和多组输入。第二,diff先算出来,避免重复写表达式,也更容易阅读。至于整型选择,头和脚的数据范围一般不会太大,int够用;但如果你看到题目限定的头数范围很大,比如超过10万,就改用long long,防止乘法和减法溢出。
3.2 Java实现
Java在OJ上的痛点主要是类名和Scanner性能。以华为OJ这样的平台为例,提交的class名必须是Main,类里面不能带package声明。代码可以写成这样:
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); while (in.hasNextInt()) { int h = in.nextInt(); int f = in.nextInt(); if (f < 2 * h || f > 4 * h || ((f - 2 * h) & 1) == 1) { System.out.println("No answer"); continue; } int rabbit = (f - 2 * h) / 2; int chicken = h - rabbit; System.out.println(chicken + " " + rabbit); } } }这里用& 1判断奇偶,其实和% 2 != 0完全等价,只是很多老程序员习惯这么写。hasNextInt()和nextInt()配合,天然支持多组输入。如果数据量非常大,可以换成BufferedReader提速,但鸡兔同笼这种题数据量一般很小,Scanner完全够用。
3.3 Python实现
Python刷OJ,输入输出一定要分清input()和sys.stdin。如果题目明确只有一组输入,用input()没问题;但如果要多组输入直到EOF,直接写input()读到底部会抛EOFError,所以最好用sys.stdin:
import sys for line in sys.stdin: line = line.strip() if not line: continue h, f = map(int, line.split()) if f < 2 * h or f > 4 * h or (f - 2 * h) % 2 != 0: print("No answer") continue rabbit = (f - 2 * h) // 2 chicken = h - rabbit print(chicken, rabbit)这里用整数除法//而不是/,是因为Python的/会得到浮点数,而OJ期望输出整数。line.strip()用来去掉可能的空行,如果某一行是空行就直接跳过,避免map(int, ...)在空字符串上报错。
3.4 自测用例设计
不管用什么语言,提交之前都建议先在本地构造一组测试数据。我常用的用例是这样:
| 编号 | 输入 h f | 期望输出 | 说明 |
|---|---|---|---|
| 1 | 3 8 | 2 1 | 正常情况 |
| 2 | 1 4 | 0 1 | 全是兔子 |
| 3 | 1 2 | 1 0 | 全是鸡 |
| 4 | 3 7 | No answer | 腿数是奇数 |
| 5 | 3 5 | No answer | 脚太少,小于2h |
| 6 | 2 9 | No answer | 脚太多,超过4h |
| 7 | 0 0 | 0 0 | 空笼子,看题目是否允许 |
第7个用例要看题目是否允许h=0。如果题目保证h>=1,这个用例就不必构造。把测试数据按行写到本地文件中,喂给程序,逐条对比输出,能覆盖绝大多数边界条件,再去OJ提交就不容易翻车。
3.5 为什么我建议三种语言都写一遍
这道题逻辑很简单,但我仍然建议初学者把C、Java、Python各写一遍。原因不是凑代码量,而是三种语言刚好覆盖三种典型输入风格:C教你理解EOF和指针层面的底层处理,Java逼你关注类名和Scanner的循环读取,Python教你用流式读取避免EOF异常。同一个逻辑在不同语言下的写法差异,恰恰是OJ刷题最需要适应的东西。以后你换平台做题,语言不熟、输入模板不熟,往往比题目本身更耗时间。
4. 常见问题与排查技巧实录
4.1 提交WA的高频原因
WA绝对是最常见的反馈。我统计过自己带过的学生提交记录,WA通常集中在下面几个原因:
- 边界条件漏判:最常见的是忘了腿数为奇数这个无解场景。比如h=3、f=7,按公式算兔子是0.5,程序直接输出错误数值。
- 公式系数套错:题目把“脚”换成“轮子”,自行车2轮、三轮车3轮,如果还按2和4算,结果必然不对。
- 输出格式不符:题目要求输出
No answer,你写成了No Answer;题目要求先鸡后兔,你输出成先兔后鸡。 - 多组输入没处理:只读一次就结束,后续数据没有跑完。
- 多余输出或注释:某些老OJ对输出要求非常严格,代码里出现中文输出或调试注释也可能导致WA。
排查时不要急着改代码。先看判题测试报告显示的是哪类失败,再对照上面清单逐项检查。大多数时候,报告里已经暗示了失败用例的样子。
4.2 读懂判题测试报告
很多OJ会在提交结果显示“通过了X组测试中的Y组”,点击还能看到详细报告。以我熟悉的课程OJ为例,报告里通常会包含:
- 测试输入,比如“头=3,脚=7”
- 你的输出和标准输出的diff
- 判定的错误类型
如果你看到标准输出是No answer,而你输出的是1 1,问题基本出在奇偶性判断。如果你看到所有输出内容都对,但反馈是PE,那就要检查每行末尾是否少换行、数字之间是否多空格。判题测试报告是定位问题的最直观材料,比瞎猜代码高效得多。
4.3 本地快速自测的小技巧
最后分享一个我非常习惯的做法。本地创建一个input.txt,把多组测试数据写进去:
3 8 1 4 1 2 3 7 3 5 2 9然后运行程序时把文件内容作为标准输入:
- C程序编译后用
./a.out < input.txt - Java用
java Main < input.txt - Python用
python main.py < input.txt
对比输出是否符合预期。这样做的好处是,提交前你能肉眼确认多组输入、空行、大小写这些细节,还能一次性回归所有边界用例。等代码稳定后再把这个文件清理掉,不影响项目目录整洁。
4.4 一个真实的翻车案例复盘
有一回我在西科大OJ平台上帮同学看这道题,他的逻辑一眼看去没问题,兔子数、鸡数都算对了,但提交就是WA。我把他的代码拿来跑本地自测用例,全过。后来我干脆下载了判题测试报告,发现失败用例是一个特别大的头数范围。再回头看代码,他定义的是int h, f;,计算4 * h时发生了整数溢出,正确答案被截断成了负数,边界判断自然全乱了。改成long long之后一次通过。这个案例给我的教训是:别看题目简单,数据范围描述一定不能跳过,乘法溢出在简单的题里照样能要命。
5. 这道题给我的练习体会
刷题刷了很多年,回头看OJ1004这种“相当于鸡兔同笼问题”的题,反而觉得它最有教学价值。它小到不需要设计算法,但又大到逼你把输入输出、判题规则、边界条件全部摸清。我在实际练习中感受最深的是:很多人在简单题上花的时间其实不少,只是没花在看得见的维度。代码的核心逻辑就三行,真正决定能不能AC的是对细节的敬畏。
从这道题之后,我养成了几个固定习惯:第一,每道OJ题先读输入输出约束再动手写代码;第二,写完先用边界条件自测再提交;第三,遇到WA先拉判题测试报告,不瞎猜。第一次提交就AC不是靠运气,而是靠把可能的坑提前踩完。建议你也找自己学校或常用的OJ平台,比如西科大OJ平台、东华OJ,或者干脆去华为OJ上搜一道鸡兔同笼变体,把整条流程走一遍。等你真正读懂一份判题测试报告里的隐藏用例,OJ平台的判题逻辑在你眼里就不再是黑盒了。