如果你也在做一个“每天练一道编程题”的自学计划那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 从题目到代码的翻译步骤很多时候写不出代码不是不会语法而是不知道怎么从题目跳到代码。我给自己定了一个固定套路先读题把数学公式写在纸上再确定输入和输出然后写伪代码最后才写真正的代码。以三角形为例读完题先在纸上写判断条件之一是“任意两边之和大于第三边”。伪代码就写如果abc并且acb并且bca说明能构成三角形。接着再想能不能简化排序之后只需要判断abc。这个“先纸上、后代码”的过程能帮你避免大部分因为思路混乱导致的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这时候另一个数就是答案。举个例子a36b24。第一步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。如果a0b5最简比应该是0:1如果a5b0除法本身没有意义。代码必须对零值单独判断否则会直接崩溃或者输出奇怪的东西。第二个是负数处理。很多题面没明说但测试数据里偷偷夹了负数。如果你不处理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余217 % 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[i1]) 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 判定合法三角形两边之和大于第三边三角形题的标准起手式是给出三条边长判断它们能否构成一个三角形。数学课上背过的是“三角形任意两边之和大于第三边”。这句话翻译成代码最直接的做法是把三条边两两配对做三次判断abc、acb、bca。但是这三个条件里其实有冗余。仔细想想如果先把三条边从小到大排好序让a是最短边、c是最长边那只需要判断abc就够了。为什么因为abc如果成立acb和bca就必然成立。ac肯定比b大因为c比b大bc肯定比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不行因为12等于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[i1]) z float(data[i2]) 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×34×4255×525看着没问题但我写的时候是拿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 1Acute triangle等边三角形最小合法情况1 1 2Not a triangle两边之和等于第三边1 1 3Not a triangle明显不合法3 4 5Right triangle经典直角三角形5 12 13Right triangle另一组勾股数2 2 3Obtuse triangle4小于9钝角4 4 5Acute triangle32大于25锐角0 1 1Not a triangle出现0的情况-1 2 3Not a triangle负数情况你可能注意到了我在表里把0和负数也算进去了。有些题目只保证输入正整数但如果题面没说程序就不能假设。稳健的程序应对非法输入给出不会崩溃的结果而不是抛异常。6.3 继续往下走的经验Day6能做下来说明你已经具备了一种很重要的能力把模糊的题意拆成精确的条件。比值让你理解“化简需要公约数”倍数让你熟练使用%三角形让你学会“先排序再判断”。这三块合起来就是接下来刷“条件判断综合题”的底气。我个人这一天刷下来的体会是三角形那题最值得反复做因为它足够小却包含了排序、条件分支、浮点误差、边界测试好几个关键点。如果今天时间有限我建议你一定要把5.3那版完整代码自己敲一遍再改动一下比如改成C版本或者增加一个“输出面积”的功能。多折腾一次比多刷三道新题更有用。今天的内容就记录到这里。明天开始这些判断逻辑会往更复杂的场景里走比如多个条件的优先级、嵌套判断、循环里的终止条件。希望这份记录能给你搭一块结实的跳板咱们下一天继续。