如果你也在做一个“每天练一道编程题”的自学计划,那Day6应该已经慢慢找到感觉了。前面五天,多半还在跟输入输出、if判断、循环这些基础语法较劲,到了今天,也就是“奇妙的比值、T的倍数N、三角形”这一组题,画风开始变了:题目不再只是“读两个数、做个加减法”,而是把你往数学思维和逻辑拆分上带。今天的三个关键词——比值、T的倍数N、三角形,看起来各不相干,本质上都在考同一件事:怎么把一个数学描述,转换成计算机能执行的判断和计算。
这篇文章不会只贴三段代码就完事。我会把每一道题背后的数学逻辑、代码实现、边界条件、我实际踩过的坑,都摊开来讲清楚。适合刚开始学编程、准备参加校内机考或想系统刷题的朋友,也适合带小朋友做编程启蒙的家长照着用。如果你已经有一定基础,可以直接跳到第5章看三角形判定里关于浮点精度和排序判型的部分,那是我今天最想分享的东西。
1. 三个知识点为什么被放在同一天
很多人刷题喜欢按“数组”“字符串”“模拟”这种标签去分类,但仔细看Day6这三道题,你会发现它们更像是一次数学思维的集中训练。把三个不同主题放在同一天,不是随意拼凑,它们背后共享同一套底层能力:把数学语言翻译成代码逻辑。
1.1 比值:从“差”到“倍”的跨越
小学生学“比”的时候,老师会说“比就是两个数相除”。比如班级里男生20人,女生15人,男生和女生的人数比就是20:15,再化简成4:3。这里的核心是化简:找出两个数的最大公因数,把两边同时除以这个数。
到了编程里,“求比值”就变成了“求最大公约数”。你可能会问:这不是数学课的事吗?为什么编程题也要考?因为程序里表达一个化简结果,不能像人一样看一眼就写出来。你输入的是两个可能很大的整数,程序必须每次都按固定步骤去算。这个固定步骤,就是算法。而最大公约数的经典算法——辗转相除法,是学习“算法”这个概念时绕不开的第一个门槛。
比值题目真正想练的不是除法本身,而是“化简”这个动作背后对约束条件的敏感度。比如除以零的问题、负数要不要处理、两个数交换顺序后结果是否一致。这些细节往往比主体逻辑更考验一个初学者。
1.2 T的倍数N:取模运算的隐藏威力
“T的倍数N”这个标题,我第一次看还愣了一下。琢磨了一会儿才确认,它想表达的是:给你一个数T和一个数N,判断N是不是T的倍数,或者找出N以内所有T的倍数。
这种题在数学上简单得像常识,但编程里它引出了一个很关键的操作符——取模,也就是%。N % T算的是N除以T的余数。如果余数为0,说明N能被T整除,N就是T的倍数。就这么简单。
可为什么单独把这题放到Day6?原因在于取模运算远不止判断倍数这么简单。它在后面会频繁出现在各种场景里:判断奇偶性、循环队列、哈希表冲突处理、日期计算、数位拆分……Day6这题更像是一次“提前打招呼”,让你先记住%的存在感,别再只会用/和*。
1.3 三角形:从图形到逻辑判断
三角形这题,图形只是壳,核心是逻辑判断。给你三条边a、b、c,先判断能不能构成三角形,再判断是锐角、直角还是钝角三角形。
这题在日常刷题里非常经典,但它练的其实是一个比“公式翻译”更高级的能力:条件拆分和去重。一个三角形有几条判定规则?两边之和大于第三边是对任意两条边都成立,还是只需要判断一次?判断直角时,如果用户给的边长顺序是3、5、4,程序该怎么知道哪条边是斜边?这些问题的答案,都要靠逻辑梳理来解决。
如果你直接把三种情况全都写进if里,代码又长又容易漏。但如果先排序,再统一判断,整个逻辑会清爽得多。这才是这道题真正想教给你的东西:很多判断问题,可以通过预处理来简化后续逻辑。
2. 准备工作和工具选择
在开始写代码之前,先把环境和工作流确认好。Day6的三道题都不复杂,但过程规范一点,后面刷到复杂题目的时候会轻松很多。
2.1 用什么语言写
如果你是在校学生,学校机考用的C++,那就继续用C++。如果你的目标是快速验证想法、把精力集中在算法思路上,Python会更舒服一些,因为不需要处理头文件、命名空间这些和题目无关的东西。
我个人现在习惯用Python来讲解这一类数学判断题,因为它长得很像伪代码。你写一个a, b = b, a % b,读代码的人一眼就能看出这是辗转相除法。用C++当然也能写,但初学者很容易被类型声明分散注意力。
今天的示例代码我统一用Python 3来实现。你看懂思路之后,再用C++写一遍,效果最好。
2.2 开发环境与调试手段
环境方面,装一个Python 3.8以上版本,再加一个顺手编辑器就够了。我用的是VS Code,装好Python插件之后可以直接在终端里运行脚本。不建议一开始就去折腾复杂的IDE,因为这三道题的规模,一个单文件就足够了。
调试手段也很重要。很多人习惯写一大段代码再一次性运行,结果报错了就要从头找。我更推荐在函数内部多写几个临时打印,把中间变量打出来。比如求最大公约数的时候,每轮循环都打印一下a和b的值,你就能亲眼看到辗转相除的过程是怎么收敛的。这种“观察中间过程”的习惯,比任何调试器都实用。
另外,我强烈建议养成“先手工算一遍,再跑程序”的习惯。比如比值题你拿36和24手工化简到3:2,然后运行程序看看输出是不是3:2。如果不一样,说明代码里有问题,这时候不要瞎猜,回到代码里对照每一步,很快就能定位。
2.3 从题目到代码的翻译步骤
很多时候写不出代码,不是不会语法,而是不知道怎么从题目跳到代码。我给自己定了一个固定套路:先读题,把数学公式写在纸上;再确定输入和输出;然后写伪代码;最后才写真正的代码。
以三角形为例,读完题先在纸上写:判断条件之一是“任意两边之和大于第三边”。伪代码就写:如果a+b>c并且a+c>b并且b+c>a,说明能构成三角形。接着再想:能不能简化?排序之后只需要判断a+b>c。这个“先纸上、后代码”的过程,能帮你避免大部分因为思路混乱导致的bug。
3. 比值问题的完整实现
3.1 数学原理:为什么要除以最大公约数
先看一道典型的比值题:输入两个正整数a和b,输出它们的最简整数比,比如36和24,应该输出“3:2”。
为什么要化简?因为从数学意义上说,36:24就是3:2,只是数值表现形式不同。程序里要输出“最简单”的形式,就必须找到那个能把两个数同时整除的最大整数,也就是最大公约数。36和24的最大公约数是12,两边同时除以12就得到3:2。
求最大公约数最常见的方法是欧几里得算法,也叫辗转相除法。它的核心是一个很巧妙的观察:两个数a和b的最大公约数,等于b和a除以b的余数的最大公约数。反复套用这个过程,余数会越来越小,直到变成0,这时候另一个数就是答案。
举个例子,a=36,b=24。
第一步:36除以24,商1余12。于是问题变成求24和12的最大公约数。
第二步:24除以12,刚好整除,余数为0。所以12就是36和24的最大公约数。
这个过程如果用代码写,就是几行的while循环。但你别小看这短短几行,它把“大问题变成同样形式的小问题”这种递归思想体现得淋漓尽致。后面学递归的时候,你会发现辗转相除法的思维又会出现一次。
3.2 代码实现与运行测试
下面先写一个求最大公约数的函数,再写一个化简比值的函数。
def gcd(a, b): while b != 0: a, b = b, a % b return a def simplify_ratio(a, b): if a == 0 or b == 0: return "0" if a == 0 else "0:1" g = gcd(abs(a), abs(b)) a //= g b //= g if b < 0: a = -a b = -b return f"{a}:{b}" print(simplify_ratio(36, 24)) # 3:2 print(simplify_ratio(15, 25)) # 3:5 print(simplify_ratio(-8, 12)) # -2:3这段代码里,gcd用了辗转相除法,simplify_ratio里先取绝对值算出最大公约数,再同时整除。最后那个if b < 0的处理,是为了保证输出的分母是正数,让格式更统一。
运行一下,输出分别会是:
3:2 3:5 -2:3你可能会问,为什么gcd里要对负数取绝对值?因为Python的取模运算对负数的行为,跟你小学学的“余数一定非负”不太一样。比如-8 % 3在Python里是1,而8 % -3是-1。为了避免这些奇奇怪怪的情况影响化简结果,我习惯在最开始就统一取绝对值,把正负号单独放到最后处理。
3.3 常见错误:顺序交换、零值、负数
这道题初学者最容易栽跟头的点有三个。
第一个是忘记处理0。如果a=0,b=5,最简比应该是0:1;如果a=5,b=0,除法本身没有意义。代码必须对零值单独判断,否则会直接崩溃或者输出奇怪的东西。
第二个是负数处理。很多题面没明说,但测试数据里偷偷夹了负数。如果你不处理,gcd算出来的公约数是个负数,最后化简结果会带着一堆负号,看起来就很不对劲。
第三个是输出格式。题目可能要求用空格隔开、用冒号隔开,或者输出a/b的形式。我吃过一次亏,题目要求输出3:2,我输出了3 2,怎么提交都是答案错误。后来才发现问题不在算法,而在格式化。所以写代码之前,一定要先看清楚题目给出的输出样例,不要想当然。
4. T的倍数N:代码里的整除判断
4.1 从数学定义到运算符
这题的数学定义,一句话就能说完:若整数N除以整数T的余数为0,则N是T的倍数。
对应到代码里,就是N % T == 0。这个式子看起来平淡无奇,但背后藏着好几个值得深挖的点。
先说说取模运算的底层逻辑。整数除法里,N除以T其实在问两个问题:商是多少?还剩多少?比如17除以5,商3余2,17 % 5的结果就是2。这个余数拿来做判断,就是“能不能整除”的试金石。
再深一层,你还得考虑T为0的情况。任何数除以0都没有意义,取模也会直接报错。所以真正负责的代码,要先判断T是否为0。
还有负数的情况。有些题面说“N和T都是正整数”,那就简单了,直接用%。但如果题面没说,最好测试一下负数取模的行为。不同语言的处理方式不一样,在Python里,-7 % 3的结果是2,因为Python保证余数与除数同号。而在C++里,-7 % 3的结果是-1。这就意味着同一道题,换一种语言,边界情况的结果可能不同。
4.2 完整解题代码
假设题目输入多组数据,每组两个整数N和T,要求判断N是否是T的倍数。我习惯写成这样:
import sys def is_multiple(n, t): if t == 0: return False return n % t == 0 def main(): data = sys.stdin.read().strip().split() if not data: return for i in range(0, len(data), 2): n = int(data[i]) t = int(data[i+1]) if is_multiple(n, t): print(f"{n} is a multiple of {t}") else: print(f"{n} is not a multiple of {t}") if __name__ == "__main__": main()输入样例:
12 3 7 2 0 5输出:
12 is a multiple of 3 7 is not a multiple of 2 0 is a multiple of 5注意我把T等于0的情况单独拎出来返回了False。虽然题目多半不会给这种数据,但这个判断能避免程序直接报ZeroDivisionError,属于防御性编程。
如果题目要求的是“输出N以内所有T的倍数”,那只需要一个循环:
n, t = map(int, input().split()) for num in range(t, n + 1, t): print(num, end=' ')这里用range的三参数形式,从T开始,每次步进T,无论如何都比在循环里写if num % t == 0更高效。效率差别看似不大,但“用步长代替条件判断”是以后处理枚举问题的一个常用优化思路。
4.3 典型的错误与调试
第一类错误是类型没转换。从input读进来的是字符串,直接跟整数做取模运算会报错。我见过不少新手在初学阶段反复栽在这里。解决方法是任何输入都第一时间int()转掉。
第二类错误是取值范围没想清楚。如果N和T都能到10的18次方,Python的整数没问题,但C++选手就要小心long long是否溢出。即便是在Python里,如果题目要求你“列出所有倍数”,也不能真的用循环从1枚举到10的18次方,那样就算能运行,也会超时。你要么用数学方法直接算有几个倍数,要么换思路,从倍数出发而不是从每个数出发去判断。
第三类错误是输出格式里的空格换行。练习系统的比对通常是整段输出完全一致,多一个空格、少一个换行都算答案错误。我的经验是,先看输出样例里有没有行尾空格,如果样例看不出来,就用最简单的方式打印:一行一个结果,中间用空格还是换行根据题目描述来。
5. 三角形判定与分类
5.1 判定合法三角形:两边之和大于第三边
三角形题的标准起手式是:给出三条边长,判断它们能否构成一个三角形。
数学课上背过的是“三角形任意两边之和大于第三边”。这句话翻译成代码,最直接的做法是把三条边两两配对做三次判断:a+b>c、a+c>b、b+c>a。
但是,这三个条件里其实有冗余。仔细想想,如果先把三条边从小到大排好序,让a是最短边、c是最长边,那只需要判断a+b>c就够了。为什么?因为a+b>c如果成立,a+c>b和b+c>a就必然成立。a+c肯定比b大,因为c比b大;b+c肯定比a大,因为c比a大。排序把三次判断简化成一次判断,这就是我前面提到的“预处理简化逻辑”。
代码可以写成:
def is_triangle(x, y, z): sides = sorted([x, y, z]) return sides[0] + sides[1] > sides[2]调试的时候可以多测几组:3、4、5可以;1、2、3不行,因为1+2等于3,刚好贴在一起无法构成面积;1、2、10更不行。等于的情况特别容易漏,很多人的条件里只写了大于,忘记“等于也不行”,结果边界数据就把程序打回原形。
5.2 直角三角形、锐角三角形、钝角三角形
能构成三角形只是前半段。如果题目进一步要求你区分直角三角形、锐角三角形和钝角三角形,就要引入边角关系的另一个定理:在三角形中,如果最长边的平方等于另外两边平方之和,那就是直角三角形;如果最长边的平方大于另外两边平方之和,那就是钝角三角形;如果小于,就是锐角三角形。
这段话用代码写出来也不难:
def triangle_type(x, y, z): sides = sorted([x, y, z]) a, b, c = sides if a + b <= c: return "Not a triangle" p = a * a + b * b q = c * c if p == q: return "Right" elif p > q: return "Acute" else: return "Obtuse"看到这你可能会想:判断直角用p == q靠谱吗?如果边长是整数,那没问题,因为两条整数的平方和要么等于第三边平方,要么不相等,没有近似一说。但如果边长是小数,比如输入是1.0、1.0、1.414,浮点运算可能会得到1.999999或2.000001,这时候用==就会误判。
解决方法是设定一个误差范围:
eps = 1e-9 if abs(p - q) < eps: return "Right"这种“浮点比较加误差范围”的处理方式,在计算几何题目里几乎是一种标准套路,今天先打个预防针。
5.3 编写通用函数:排序 + 判断
把上面两段合并一下,写成一个完整的三角形分类器,并且加上main函数处理输入输出:
import sys def classify_triangle(x, y, z): sides = sorted([x, y, z]) a, b, c = sides[0], sides[1], sides[2] if a + b <= c: return "Not a triangle" p = a * a + b * b q = c * c eps = 1e-9 if abs(p - q) < eps: return "Right triangle" elif p > q: return "Acute triangle" else: return "Obtuse triangle" def main(): data = sys.stdin.read().strip().split() if not data: return for i in range(0, len(data), 3): x = float(data[i]) y = float(data[i+1]) z = float(data[i+2]) print(classify_triangle(x, y, z)) if __name__ == "__main__": main()输入:
3 4 5 5 5 5 2 2 3 1 2 3输出:
Right triangle Acute triangle Obtuse triangle Not a triangle这版代码把输入统一按float处理,既兼容整数也兼容小数。排序用的是Python内置的sorted,底层是TimSort,效率稳定,不用自己造轮子。
6. 实战过程与排错记录
6.1 我踩过的坑
第一坑:自以为最长边在最后。题目输入给的是3 5 4,我一上来就把c当成最长边,直接判断a*a + b*b == c*c,结果3×3+4×4=25,5×5=25,看着没问题,但我写的时候是拿a、b、c原始顺序算的,如果把3 4 5和3 5 4混在一起,同一个逻辑就时灵时不灵。后来才醒悟:必须先排序,让最长边固定落在c上。
第二坑:把“任意两边之和大于第三边”写成了“最大边加最小边大于中间边”。这纯粹是手滑,但程序不会提醒你逻辑错了,只会默默给出错误的判断结果。我的经验是,写完这种条件判断后,用几个自己心算过的用例先跑一遍,不要直接提交。
第三坑:浮点误差。第一次写直角判定时,我用的是math.sqrt,先算出c的平方根,再拿它和某条边比较。结果输入1 1 1.414时,程序判断成了钝角三角形。排查了半天,发现问题出在1.414不是1.41421356……,开方后误差被放大了。改用“比较平方”之后,这个问题就消失了。
第四坑:边界值测试。1 2 3这样的分组我一开始没测,提交后错在“等于第三边”的用例上。后来我把测试计划固定成几组:最小合法三角形、等边三角形、直角三角形、钝角三角形、刚好不合法、有一条边特别长。这组测试跑一遍,心里基本就有底了。
6.2 边界测试表
下面这张表是我写三角形分类题时常用的测试用例,你可以直接拿去用:
| 输入 | 期望结果 | 说明 |
|---|---|---|
| 1 1 1 | Acute triangle | 等边三角形,最小合法情况 |
| 1 1 2 | Not a triangle | 两边之和等于第三边 |
| 1 1 3 | Not a triangle | 明显不合法 |
| 3 4 5 | Right triangle | 经典直角三角形 |
| 5 12 13 | Right triangle | 另一组勾股数 |
| 2 2 3 | Obtuse triangle | 4小于9,钝角 |
| 4 4 5 | Acute triangle | 32大于25,锐角 |
| 0 1 1 | Not a triangle | 出现0的情况 |
| -1 2 3 | Not a triangle | 负数情况 |
你可能注意到了,我在表里把0和负数也算进去了。有些题目只保证输入正整数,但如果题面没说,程序就不能假设。稳健的程序应对非法输入给出不会崩溃的结果,而不是抛异常。
6.3 继续往下走的经验
Day6能做下来,说明你已经具备了一种很重要的能力:把模糊的题意拆成精确的条件。比值让你理解“化简需要公约数”,倍数让你熟练使用%,三角形让你学会“先排序再判断”。这三块合起来,就是接下来刷“条件判断综合题”的底气。
我个人这一天刷下来的体会是:三角形那题最值得反复做,因为它足够小,却包含了排序、条件分支、浮点误差、边界测试好几个关键点。如果今天时间有限,我建议你一定要把5.3那版完整代码自己敲一遍,再改动一下,比如改成C++版本,或者增加一个“输出面积”的功能。多折腾一次,比多刷三道新题更有用。
今天的内容就记录到这里。明天开始,这些判断逻辑会往更复杂的场景里走,比如多个条件的优先级、嵌套判断、循环里的终止条件。希望这份记录能给你搭一块结实的跳板,咱们下一天继续。