☰
P5717 三角形分类:排序化简与整数平方判据
2026/10/27 12:20:17 网站建设 项目流程

P5717 这道题在入门题库里只标了个"入门"难度,但它每年都能让一批人栽在评测机上——不是不会写判断,而是判断写得全不全、顺序对不对、边界卡没卡住。题目本身很朴素:给三条整数边长,先回答能不能拼成三角形,能拼的话再依次说明它是直角、锐角还是钝角,是等腰还是等边。听起来就是几个 if 的事,可真动手写,排序忘了、不等式写错、平方用 double 比、等边漏输出等腰、输出顺序颠倒,这些坑一个接一个。它把新手最容易含糊的几件事——排序化简、三角形不等式的等价变形、用整数平方替代开方、多条件叠加输出——全塞进了二十行代码里。

我把它当成一块试金石:一个人写判断逻辑是否严谨、对边界是否敏感,看这道题就够了。下面我按自己带人做题、自己复盘踩坑的顺序,把这道题从题面到代码、从判据到测试、从坑位到延伸完整走一遍,代码给 C++ 和 Python 两个版本,测试用例给一套我常用的组合,新手可以照着抄,有基础的朋友可以重点看判据推导和踩坑那一节。

1. 先把题目掰开揉碎:三角形分类到底在考什么

1.1 输入输出约定先对齐,别急着写代码

这道题的输入只有一行三个整数,代表三条线段的长度,题目约定长度是不超过一万的整数。输出的讲究就多了:如果三条边拼不成三角形,只输出一行Not triangle,别的一概不许有;如果能拼成,则按固定顺序输出若干行——先判断这个三角形的最大角是直角、锐角还是钝角,对应Right triangle、Acute triangle、Obtuse triangle,三者互斥,只会出现一个;接着判断它是不是等腰,是就输出Isosceles triangle;最后判断是不是等边,是就输出Equilateral triangle。

这里有个非常关键但题面不会明说的规则:多个条件同时满足时,要全部输出,而且顺序不能错。一个等边三角形同时也是等腰三角形,所以它必须连续输出Acute triangle、Isosceles triangle、Equilateral triangle三行。很多人第一次提交只输出一行Equilateral triangle,评测机直接判错,回头翻题面才发现"同时满足要全部输出"这句话藏在角落里。

再强调一遍输出字符串的准确性,这是纯粹的白给分,也是纯粹的白给扣分点。大小写敏感,全部是首字母大写、其余小写,单词之间有且只有一个空格,行末不要有空格。我把五个答案加一个否定答案列成表,写代码的时候直接对着抄,别凭记忆敲:

含义正确输出常见错误写法
直角三角形Right triangleright triangle、Right Triangle
锐角三角形Acute triangleAcute triangel
钝角三角形Obtuse triangleObtuse triangle(多余空格)
等腰三角形Isosceles triangleIsoceles triangle(少一个 s)
等边三角形Equilateral triangleEquilateral
无法构成Not trianglenot triangle、Not a triangle

Isosceles这个词是拼错重灾区,中间是scel,不是scel也不是scele,我见过不止一个人卡在这上面半小时,最后发现是字母顺序敲反了。这种错误排查起来最费劲,因为逻辑全对,就是字符串不对,评测机只告诉你答案错误,不告诉你哪里错。

1.2 为什么"先排序"是这道题的第一个关键动作

拿到三条边,绝大多数人的第一反应是直接写三个判断:a+b>c、a+c>b、b+c>a,三条都成立才算三角形。这个写法没错,但它把后续所有判断都变复杂了。因为在没排序的情况下,你永远不知道哪条边最长,而"最大角"的判断必须依赖最长边,等腰判断也得老老实实写三组两两比较。

排序的价值就在这里:把无序变有序,把"三选一"变成"唯一确定"。排完序后记x≤y≤z,那么z一定是最长边,z对应的角一定是最大角,判断三角形类型只需要盯住这一组x²+y²和z²的关系;判断等腰只需要看x==y或y==z,因为x==z必然蕴含三者全等,根本不用单独写;判断等边只需要x==y && y==z。代码量直接砍掉一半,可读性还上去了。

我在实际写题时有个固定习惯:只要题目涉及"最长边""最大角""两两比较",先花一行sort把数据摆平,再动脑子。这一行sort的成本可以忽略,收益是把后面所有的分支逻辑都压平了。很多人觉得排序是多余的,三行比较而已,但等你写到第五个判断条件,自己都记不清哪个变量是最大边的时候,就知道这行sort值多少钱了。

还有个小细节:排序之后变量的语义变了。如果你用a、b、c三个变量接收输入,排完序之后a不一定还是第一条输入的边。这本身没问题,但如果代码里既有"原始输入"又有"排序后"的逻辑混着用,就会出大问题。我的做法是输入用数组或列表接,排序后立刻转成x、y、z三个新变量,此后所有判断只用x、y、z,从根上避免混用。

2. 两个核心判据:三角形不等式与勾股定理的正确姿势

2.1 三角形不等式:三组比较怎么压成一次比较

三角形不等式的内容是"任意两边之和大于第三边"。三边记为a、b、c,就要同时满足a+b>c、a+c>b、b+c>a。这三条是"且"的关系,缺一不可。

排序成x≤y≤z之后,可以证明只需要验证x+y>z这一条,另外两条自动成立。推导过程很直白:因为x≤y≤z,所以x+z ≥ x+y,而x+y>z已经成立,于是x+z > z ≥ y,即x+z>y;同理y+z ≥ x+y > z ≥ x,即y+z>x。换句话说,较小的两条边加起来都能超过最大边,那么任何一条边加上最大边,必然超过剩下的那条边——毕竟最大边本身都不小于另一条边了。

这个化简不是炫技,它直接影响代码的正确性。我见过最常见的错误是:没排序,却只写了一个a+b>c。测试数据1 2 3传进来,1+2>3不成立,判成不能构成三角形,侥幸对了;但数据3 1 2传进来,3+1>2成立,程序就认为能构成三角形,直接错判。顺序一变结果就变,这是典型的不排序硬套公式的后果。

另一个必须注意的点是"取不取等号"。三角形不等式是严格大于,不是大于等于。1 2 3三条边,1+2=3,拼出来的是一条直线,三个顶点共线,这不是三角形,是退化情形,必须输出Not triangle。同理1 1 2、5 5 10都是退化情形。代码里写成x+y<=z就判掉,不要写x+y<z,差一个等号就是两类数据的区别。

2.2 直角、锐角、钝角的统一判据

初中课本讲的是勾股定理:直角三角形满足a²+b²=c²,其中c是斜边。但题目还要判断锐角和钝角,光靠勾股定理不够,得用它的推广形式——余弦定理。

设最长边z所对的角为C,余弦定理给出cos C = (x²+y²-z²)/(2xy)。分母2xy是正数(边长都为正),所以cos C的正负完全由分子x²+y²-z²决定。而余弦函数在 0 到 180 度之间单调递减,于是:

  • 分子等于 0,cos C = 0,C = 90°,直角三角形;
  • 分子大于 0,cos C > 0,C < 90°;又因为z是最大边,其余两角必然更小,所以三个角都小于 90°,锐角三角形;
  • 分子小于 0,cos C < 0,C > 90°,钝角三角形。

为什么只判断最长边对应的角就够?因为三角形内角和是 180 度,最多只能有一个角大于等于 90 度;而"大边对大角",最长边对应的就是最大角。最大角是锐角,其他角不可能跑出锐角范围;最大角是钝角,这个三角形就是钝角三角形。一次比较解决全部问题,这就是排序带来的红利。

代码层面,我强烈建议用整数平方直接比较,不要开方,不要算余弦。边长上限一万,平方是10⁸量级,两个加起来2×10⁸,完全在 32 位有符号整数的安全范围内(约2.1×10⁹)。不过我还是习惯用long long,理由很简单:以后遇到边长上限更大的题,改都不用改,省得某天被int溢出背刺一次。

2.3 等腰、等边与"叠加输出"的顺序问题

等腰和等边的判定是纯相等比较,排序后写起来极短:x==y || y==z就是等腰,x==y && y==z就是等边。这里有个容易忽略的点:因为已经排序,x==z必然意味着三者全等,所以等腰判断不需要把x==z也写进去。当然你写全了也不错,只是冗余。

真正的坑在输出顺序。题目要求的顺序是:先角类型(直角/锐角/钝角三选一),再等腰,再等边。这意味着等边三角形要输出三行,而不是一行。很多人的代码长这样:先判断是不是等边,是就输出等边然后return,结果等边三角形那一组数据永远只输出一行,判错。

正确的写法是把判断拆成互不干扰的三段:第一段用if-else if输出角类型,保证三选一;第二段独立if输出等腰;第三段独立if输出等边。三段之间不要互相干扰,不要提前return。这个结构我称为"分块判断、顺序输出",比把所有条件揉进一个大if-else链条里要清晰得多。

另外提一个有意思的数学事实:整数边长的等腰直角三角形是不存在的。设两条直角边都是a,斜边就是a√2,要让斜边也是整数,a必须是非零有理数乘√2还能得到整数,这不可能。所以在整数输入下,"Right triangle和Isosceles triangle同时出现"这种情况根本不会发生。知道这一点不能帮你少写代码,但能帮你在对拍时判断某组输出是否可疑——如果程序在整数输入下输出了直角加等腰,多半是判断逻辑串了。

3. 从思路到代码:完整实现与逐行解读

3.1 C++ 版本:整数运算,一次排序走天下

先给完整实现,再逐段说为什么这么写:

#include <bits/stdc++.h> using namespace std; int main() { long long a[3]; cin >> a[0] >> a[1] >> a[2]; sort(a, a + 3); long long x = a[0], y = a[1], z = a[2]; // 排序后只需比较最小两边之和与最大边 if (x + y <= z) { cout << "Not triangle\n"; return 0; } long long left = x * x + y * y; long long right = z * z; if (left == right) cout << "Right triangle\n"; else if (left > right) cout << "Acute triangle\n"; else cout << "Obtuse triangle\n"; if (x == y || y == z) cout << "Isosceles triangle\n"; if (x == y && y == z) cout << "Equilateral triangle\n"; return 0; }

几个设计决策值得说一下。用cin连续读三个数而不是按行读,是因为cin会自动跳过空白字符,输入是空格分隔还是换行分隔都无所谓,这在评测环境里能省掉一类格式问题。用数组接输入而不是三个独立变量,是为了能直接sort,sort(a, a+3)对三个元素排序的开销可以忽略,但省去了手写交换逻辑。

Not triangle分支里的return 0是有意为之。一旦确认构不成三角形,后面所有平方比较都没有意义,直接终止,杜绝程序继续跑下去输出脏数据的可能。我见过有人这里用if-else包住后面全部逻辑,嵌套三层之后自己都看不清括号配对,用提前返回是最省心的写法。

角类型判断用if-else if-else三选一,等腰和等边用两个独立if,这个结构对应题目"多条件叠加"的要求。注意等边判断没有写成else if,因为等边一定等腰,写成else if会让等边三角形永远等不到第二行输出。

3.2 Python 版本:短是短,读入要小心

Python 写起来更短,但读入环节有个坑必须先处理:

import sys data = sys.stdin.read().split() x, y, z = sorted(map(int, data[:3])) if x + y <= z: print("Not triangle") sys.exit(0) left, right = x * x + y * y, z * z if left == right: print("Right triangle") elif left > right: print("Acute triangle") else: print("Obtuse triangle") if x == y or y == z: print("Isosceles triangle") if x == y == z: print("Equilateral triangle")

为什么不用input().split()?因为那个写法默认三个数在同一行。虽然这道题的题面通常就是一行三个数,但评测环境里换行读入的情况并不罕见,用sys.stdin.read().split()一次性把所有空白分隔的内容读进来,无论空格、换行、制表符都不会出错。这是我自己写题时的默认模板,多打几个字符,换一份安心。

sorted()返回的是新列表,直接解包成三个变量,比用索引取干净。x == y == z是 Python 特有的链式比较,等价于x == y and y == z,读起来更像数学写法。sys.exit(0)和 C++ 的return 0作用一致,确认不是三角形之后立即结束,别让程序继续往下跑。

有人会问:Python 的大整数没有溢出问题,为什么还要提long long?因为在 C++ 那边有,而且这道题的边长上限一万只是个具体数字,换一道边长上限 10⁹ 的题,平方后就是 10¹⁸ 量级,必须上 64 位。养成用大一点的类型的习惯,比每次都要停下来算一遍边界要高效得多。

3.3 测试用例设计:我习惯跑的这八组

写完代码不许直接交,先自己跑一遍。我固定用下面这组数据过一遍,覆盖直角、锐角、钝角、等腰、等边、退化、乱序七种情况:

输入排序后判据过程期望输出
3 4 53 4 57>5,9+16=25Right triangle
3 3 33 3 36>3,18>9Acute triangle/Isosceles triangle/Equilateral triangle
2 3 42 3 45>4,4+9=13<16Obtuse triangle
5 5 75 5 710>7,50>49Acute triangle/Isosceles triangle
10 10 1910 10 1920>19,200<361Obtuse triangle/Isosceles triangle
1 2 31 2 33<=3Not triangle
1 1 21 1 22<=2Not triangle
5 4 33 4 5同第一组Right triangle

第三组2 3 4和第四组5 5 7是我特意选的"临界钝角/临界锐角"样本。2 3 4的平方和比斜边平方小 3,是非常典型的钝角;5 5 7的平方和只比斜边平方大 1,稍微算错一点就会误判成直角,拿来检验整数比较是否写对特别有效。

第五组10 10 19用来打脸"等腰一定是锐角"的错觉。等腰三角形的顶角完全可以超过 90 度,输出Obtuse加Isosceles两行才正确。第六、七组是退化三角形,专门验证那个<=有没有写成<。第八组是乱序输入,验证排序有没有真的生效——如果没排序还硬套公式,这组必然出错。

4. 踩坑实录:新手在这道题上翻车的六个位置

4.1 高频错误对照表:先看别人怎么错

带人做题这些年,这道题上的错误高度集中,就那么几类,我整理成表,写完代码逐行对照一遍,比漫无目的地读代码有效:

错误现象根本原因正确做法
3 1 2判成能构成三角形没排序就只写a+b>c先排序,再比较x+y与z
1 2 3判成能构成三角形三角形不等式写成<而不是<=判掉用x+y<=z判定不能构成
等边三角形少输出等腰那一行判断用了else if嵌套等腰、等边用两个独立if
直角三角形判成锐角平方比较方向写反记牢:和大为锐,和小为钝
某组数据随机答案错误用了sqrt或double比较一律整数平方比较
编译没过或运行崩溃数组越界或未初始化用vector或固定长度数组并全部赋值

表格里最后一条看着不起眼,但确实发生过:有人为了少写变量,把三条边塞进长度 2 的数组里,编译能过,运行时读到垃圾值,输出什么全看运气。这类问题在本地小数据上可能碰巧正确,一到评测机就原形毕露。

还有一个隐蔽的坑是"判断完角类型后忘了继续判断等腰"。因为角类型那三个分支里有的写了return,有的没写,逻辑不统一,导致某些输入走到一半就退出了。我的建议是:除了Not triangle这一处提前返回,其余位置一律不用return,让程序自然执行到最后一行,输出结构一目了然。

4.2 浮点数的诱惑:sqrt和acos的灾难

很多人学完余弦定理,第一反应是算出角再判断:C = acos((x*x+y*y-z*z)/(2.0*x*y)),然后看C和π/2谁大。思路完全正确,但工程上是自找麻烦。

第一,浮点数的相等比较本身就不可靠。理论上直角对应分子恰好为 0,但x*x+y*y-z*z是整数运算,结果是精确的 0,一转成double再参与除法、再进acos,中间任何一步都可能引入误差,得到1.5707963267948966或者1.5707963267948968,你再用C == M_PI/2去比,大概率不成立。工程上正确的做法是设一个容差eps=1e-9,判断fabs(C - M_PI/2) < eps,但这又多引入一个"容差取多大"的调参问题,边长极小时误差会被放大,容差太小判不出直角,太大又把接近直角的锐角误判成直角。

第二,整数平方比较是零成本的精确方案。x*x+y*y和z*z都是整数,相等就是相等,不等就是不等,不存在模糊地带。既然题目给的是整数边长,判据又能完全用整数表达,为什么要绕到实数域再绕回来?

第三,浮点运算慢且不可移植。虽然这道题的数据规模小到无所谓性能,但养成"能用整数就不用浮点"的习惯,在计算几何题里能救命。我自己的原则是:只要比较的是"平方和的大小关系",永远不开放根号;只有在需要输出具体长度值的时候,才在最后一步开方。

4.3 几条只有踩过才记得住的心得

第一条心得:先写"不能构成"的分支,再写其余。原因是这个分支是唯一的否定分支,逻辑最简单,先把它落地能立刻锁定一半的边界问题。而且一旦确认不能构成,后面的所有判断在逻辑上都不该执行,用提前返回把这个约束强制住,比事后检查要可靠。

第二条心得:把"排序后变量含义变了"这件事写在注释里。我自己的模板会在排序那行后面加一句// 此后 x<=y<=z,看着多余,但隔一周回来看代码时,这一行能省掉三分钟的重新推理。团队协作时更有用,别人接手你的代码,第一眼就知道变量约定。

第三条心得:输出字符串统一用复制粘贴,不要手敲。我建议把五个答案先单独写在一个地方,检查拼写无误,后面所有输出语句从这里复制。手敲Isosceles出错概率极高,而这类错误调试成本又不低,属于典型的"预防成本远低于修复成本"。

第四条心得:本地测试一定要测乱序输入。这道题的正确解法对顺序不敏感,错误解法对顺序极度敏感,所以乱序数据是区分对错最快的探针。我通常准备至少两组乱序,一组是普通乱序,一组是"最大边在第一位"的极端乱序,后者专门打击"默认最后一个是最大边"的错觉。

5. 把这道题做厚:三个值得动手的改造方向

5.1 改造一:从三条边长换到三个坐标点

题目给的是边长,但实际场景里更常见的是给三个点的坐标,让你判断它们构成什么三角形。这时候第一步不是算边长,而是先判断三点是否共线——共线就是退化,直接输出不能构成三角形。

判断共线用向量叉积:设三点A(x1,y1)、B(x2,y2)、C(x3,y3),向量AB = (x2-x1, y2-y1),AC = (x3-x1, y3-y1),叉积cross = AB.x * AC.y - AB.y * AC.x。cross == 0表示三点共线,此时"三角形"退化成一条线段。这个判据同样是纯整数运算,不需要开方,也不会引入精度问题。

确认不共线之后,把三条边长算出来:AB² = (x2-x1)² + (y2-y1)²,其余同理。此时拿到的是边长的平方,正好——因为后面判断直角、锐角、钝角本来就是比较平方和,根本不用开方。排序三个平方值,用同样的判据走一遍就完成了分类。这个改造把"坐标几何"和"三角形分类"两个知识点串在一起,是很典型的组合练法。

这里有个细节要注意:坐标版的边长平方可能很大,坐标范围给到 10⁴ 的话,单条边的平方就是2×10⁸,三条边平方相加是4×10⁸,还在 32 位范围内;但如果坐标范围到 10⁹,平方就是 10¹⁸ 量级,必须用 64 位整数。这就是前面说的"用大类型省心"的实际价值。

5.2 改造二:多组数据,直到输入结束

原始题目只处理一组数据,改造一下变成多组,考验的是循环结构和状态重置。C++ 的写法是把主逻辑包在while (cin >> a[0] >> a[1] >> a[2])里,cin在读到文件末尾时返回假,循环自然结束;Python 则用for line in sys.stdin逐行处理,或者一次读完再按三个一组切片。

多组数据最容易出的问题是"状态没重置"。如果上一组的某个中间变量影响了下一组,错误会以"某些组对某些组错"的形式出现,极难定位。我的做法是:把每组数据的处理逻辑封装成一个函数,所有中间变量都在函数内部定义,天然隔离,不共享任何状态。这个习惯在后续做更复杂的模拟题时价值更大。

还有一个配套问题是输出格式。多组数据如果题目要求每组之间空一行,就要严格控制;如果要求连续输出,就不能多打空行。这类格式要求在评测里同样是硬性判错,不要觉得"多一个空行无所谓"。

5.3 改造三:把整数换成实数边长

前面反复强调用整数平方比较,是因为整数输入下这套判据无懈可击。但如果边长是实数呢?比如输入是3.0 4.0 5.0,或者更刁钻的1 1 1.41421356,整数方案就用不了了。

实数版本的思路是把所有判据改成带容差的比较。三角形不等式改成x + y > z + eps,其中eps取一个相对于数据规模合适的值,比如1e-9;平方比较改成fabs(left - right) < eps判直角,left > right + eps判锐角,left < right - eps判钝角。容差的选取原则是:大于浮点误差的累积量,小于题目关心的最小差异。边长误差如果是1e-6量级,平方后误差会放大到1e-5量级,eps就得取1e-5以上。

这个改造的意义在于让你真正理解"为什么整数输入下不需要容差"——不是因为浮点不好,而是因为这道题的判据恰好可以用整数精确表达。判据一旦涉及开方或者三角函数,容差就是绕不过去的工程现实。想清楚这一点,以后再遇到类似题目,先问自己一句"能不能全程用整数",答案能就坚决用整数,不能就老老实实设计容差。

我平时做完一道入门题,习惯花十分钟想想"如果条件变一下会怎样",这三个改造方向就是这么来的。它们不会让这道题的分数变高,但会让这道题的价值从"一道入门题"变成"一个可以反复复用的分类模板"——三角形不等式、排序化简、平方判角度、多条件叠加输出,这四个模块在后面很多计算几何题目里都会以各种形式重新出现。真正记住它们的方式不是背代码,而是像这样把边界一个个推开,看模板在哪一步失效、失效之后怎么补。补过一次,下次再遇到就不用重新想了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询