刷洛谷入门题的时候,很多人会在P5717这道“三角形分类”上栽跟头。题目本身不难,但它的判定顺序、浮点数比较、以及隐藏的多种情况分类,恰恰是新手最容易犯迷糊的地方。这篇就围绕这道题,把从读题到AC的完整思路、代码实现和踩坑记录都捋一遍,顺便聊聊这类“多分支分类”题目背后通用的解题套路。
1. 题目到底在考什么:不只是“if-else”那么简单
1.1 读懂题目要求是第一步
P5717题目的描述非常直白:给定三条线段长度,判断它们能否组成三角形。如果能,再进一步判断这个三角形是锐角三角形、直角三角形,还是钝角三角形,同时还要输出它是否为等腰三角形或等边三角形。
这里头第一个容易被忽略的点是:输出有两个维度。第一维度是“按角分类”,第二维度是“按边分类”。这两个维度不是互斥的,而是一个三角尺可以同时属于多个类别。比如等边三角形既是锐角三角形,又是等腰三角形,输出的时候需要同时体现。很多初学者只输出一个类别,自然就WA了。
另外一个隐含考点是:题目给出了三条边的顺序,但它们并不一定满足“a < b < c”之类的排列。如果直接按输入顺序去判断角的大小关系,就得写很多分支条件,代码会非常啰嗦。所以这里需要一个预处理步骤:排序。
1.2 核心考点拆解
这道题虽然挂着“入门”标签,但实际上包含了几个十分关键的编程基础技能:
- 条件分支的综合运用:不仅仅是简单的单层if,而是多层嵌套、多条件并列、优先级排列。
- 排序思维:通过将三边排序,把“判断最大边”和“比较平方和”的逻辑大大简化。这种“先排序再处理”的思路在后续很多算法题里都会反复用到。
- 浮点数的比较:三条边的长度可以是整数吗?题目并没有明确说,实际上洛谷的测试数据都是整数,但作为严谨的解法,需要考虑到边长可能不是整数的情况。用浮点数判断直角时,直接比较“a² + b² == c²”常常会因为精度问题而失败,必须使用误差范围判断。
- 数学定理的应用:构成三角形的充要条件是“任意两边之和大于第三边”,而判断角类型用的是勾股定理及其推论:最大边的平方等于另外两边的平方和是直角,大于则是钝角,小于则是锐角。
这些点单独拎出来都不难,但它们组合在一起,就成了新手测评机前的一道坎。
1.3 适合谁来刷这道题
如果你刚学完C++或Python的基本语法,正在找一些“看起来简单但暗藏细节”的题目练手,P5717非常合适。它能帮你巩固if-else嵌套、逻辑运算符的优先级、以及数据预处理的思想。同时,对于那些准备参加入门级编程比赛的同学,这种题也是必须拿满分的送分题,因为它的分类逻辑非常考察细心程度。
2. 解题思路的拆解与方案选型
2.1 为什么一定要先排序
拿到三条边,我们最自然的想法是设三条边为a、b、c,然后分类讨论谁是最大边。如果不排序,我们需要写:
- 如果a是最大边,比较a²与b²+c²
- 如果b是最大边,比较b²与a²+c²
- 如果c是最大边,比较c²与a²+b²
这样三个分支内容几乎一样,代码重复严重,还容易漏情况。更糟糕的是,判断是否能构成三角形时,如果a+b>c成立,同时还需要b+c>a和a+c>b,三个条件都得列出来。
如果先把三条边按非递减顺序排好,比如从小到大为a、b、c,那么“最大边”就是c。此时构成三角形的条件只需要一个:a + b > c。为什么?因为a和b已经是较小的两条边,只要它们的和能大于最大的c,那么其他任意两边之和也必然大于第三边(b+c>a和a+c>b显然成立,因为c已经是最大边)。同理,判断角类型时,只需要比较c²与a²+b²的大小关系就行了。一个排序,把多种情况收敛为统一模式,这就是“数据预处理”的魅力。
2.2 三角形构成的判定顺序
先判断能否构成三角形,如果连三角形都不是,直接输出“Not triangle”,后面全都不要管。这个顺序必须放在最前面。不要先判断等腰、直角那些,因为那些都是建立在“能构成三角形”的基础上。
在这里我还想强调一个细节:构成三角形的条件究竟是“a + b > c”还是“a + b >= c”?严格意义上,等于时三边共线,无法构成三角形。题目里说的三角形,默认是“非退化”的,即任意两边之和严格大于第三边。所以一定要用大于号,不能加等号。这一点在输入数据边界值(比如1 1 2)时特别容易出错,输出应该是Not triangle。
2.3 角的分类:勾股定理的推论
排序后,c是最长边。根据余弦定理或者勾股定理的扩展:
- 如果 a² + b² == c²,则夹角C是90度,三角形为直角三角形。
- 如果 a² + b² < c²,说明C大于90度,三角形为钝角三角形。
- 如果 a² + b² > c²,说明C小于90度,三角形为锐角三角形。
这里的“等于”在整数运算下可以用“==”,但如果边长是浮点数,就绝不能直接用等于。推荐使用误差判断:绝对值小于某个极小值,比如1e-6,就视为相等。
有些同学会问:如果我已经知道c是最大边了,为什么不用余弦定理直接算角度?因为那样需要用到反三角函数,不仅慢,而且精度误差更大。用平方比较是最高效且足够精确的方法。
2.4 边分类的优先级:等腰与等边的坑
按边分类时,通常的逻辑是:
- 等边三角形:三条边都相等。
- 等腰三角形:至少两条边相等。
- 不等边三角形:三条边都不相等。
这里有个常见坑:一个等边三角形同时也满足等腰三角形的定义。但是题目要求的输出顺序是“先角后边”,而且边分类里,等边和等腰都需要输出。也就是说,如果输入是3 3 3,输出应该是:
Acute triangle Isosceles triangle Equilateral triangle注意,不能只输出Equilateral triangle,还得有Isosceles triangle。反过来,如果只输出Isosceles triangle而不输出Equilateral triangle,也是错的。所以下面的分支结构要注意:
if (至少两条边相等) cout << "Isosceles triangle" << endl; if (三条边都相等) cout << "Equilateral triangle" << endl;两个if是并列关系,而不是else if。很多同学把等腰和等边写成else if,结果等边三角形只输出了等腰,丢了等边。
2.5 整体输出顺序
题目要求的输出顺序是固定的:先判断并输出Not triangle,如果有三角形,再依次判断输出:
- 是否是直角三角形、锐角三角形或钝角三角形(这三个里面必须且只能输出一个)。
- 是否是等腰三角形、等边三角形(这两个可能同时输出,也可能都不输出,注意等边一定等腰)。
在实际写代码时,角类型的三个分支用else if串联,边类型两个分支用两个独立if。这样可以保证顺序和逻辑都不乱。
3. 代码实现与关键步骤详解
3.1 C++实现
下面给出我常用的C++实现,并对每个关键部分做说明。
#include <iostream> #include <algorithm> using namespace std; int main() { int a, b, c; cin >> a >> b >> c; // 将三边从小到大排序,保证 c 是最大边 int sides[3] = {a, b, c}; sort(sides, sides + 3); a = sides[0]; b = sides[1]; c = sides[2]; // 1. 判断是否能构成三角形 if (a + b <= c) { cout << "Not triangle" << endl; return 0; } // 2. 按角分类 if (a * a + b * b == c * c) { cout << "Right triangle" << endl; } else if (a * a + b * b < c * c) { cout << "Obtuse triangle" << endl; } else { cout << "Acute triangle" << endl; } // 3. 按边分类 if (a == b || b == c) { cout << "Isosceles triangle" << endl; } if (a == b && b == c) { cout << "Equilateral triangle" << endl; } return 0; }核心要点:
- 用数组加
sort排序,比手动交换三个变量更简洁,也避免出错。当然,如果有些同学还没学到数组,也可以手动排序,但数组方式更通用。 - 排序后,
c一定是最大边。但要注意,sort默认升序,所以c就是最大值。 - 直角判定直接用
==。因为本题数据都是整数,所以没有问题。如果担心题目使用浮点,可以改成abs(a*a + b*b - c*c) < 1e-6。 - 输出英文句子,注意首字母大写,单词之间空格,结尾有换行。一个单词拼错都会WA。
- 边分类里两个if是独立的,因为等边必然等腰。
3.2 Python实现
Python版本逻辑相同,只是语法上的差异。
a, b, c = map(int, input().split()) # 排序 a, b, c = sorted([a, b, c]) if a + b <= c: print("Not triangle") exit() if a * a + b * b == c * c: print("Right triangle") elif a * a + b * b < c * c: print("Obtuse triangle") else: print("Acute triangle") if a == b or b == c: print("Isosceles triangle") if a == b and b == c: print("Equilateral triangle")注意Python的exit()可以提前结束程序,或者在画三角形判断之后用if ... else包裹后面的语句,但用exit()更清晰。不过提交洛谷时,使用sys.exit()更规范,exit()在某些环境下可能有额外输出,但洛谷的Python环境里用exit()没问题。稳妥起见也可以把后面所有输出都放在一个不构成三角形的else分支里,但那样嵌套层层太多,可读性差。我比较推荐exit()。
3.3 手动排序版(不依赖sort函数)
如果还没学到排序函数,也可以手写交换逻辑。三条边排序其实很简单,先找出最大值,再比较剩下两个。
int x, y, z; cin >> x >> y >> z; if (x > y) swap(x, y); if (y > z) swap(y, z); if (x > y) swap(x, y);三段交换后,x <= y <= z。原理类似冒泡排序的简化版,这也是一个不错的练习。但实际做题时直接用sort更高效,毕竟排序不是本题的学习重点。
3.4 参数与数据范围分析
洛谷的P5717原题没有明确说明输入数据范围,但从“深基3.习8”的定位推断,三边长度是正整数,且不大于1000。因此用int完全够用。平方之后最大是1000*1000=1,000,000,三个平方和也不超过3,000,000,int没问题。但有些变式题可能边长很大,比如到10^9,平方就超出int范围,需要用long long。为了稳健,我见过有人直接定义long long,这样保险又不吃亏。如果你平时做题习惯用int,也请记得在乘法面前多加一个long long转换。比如1LL * a * a + ...,防止意外溢出。虽然本题用不上,但这是一个好习惯。
3.5 一个易错点:输出字符串的精确匹配
洛谷的判题机对输出要求极其严格,大小写、空格、换行都不能错。有些题目的提示里写了“Right triangle”而不是“right triangle”,哪怕首字母大写错了都会判WA。还有“Isosceles”和“Equilateral”这两个单词,一个拼错就会前功尽弃。我建议提交前把题面里的英文原样复制出来,或者在自己代码里一个一个字母对一遍。不要靠记忆敲,人的记忆对英文单词的末尾拼写很容易出错。
4. 代码提交后的常见问题与排查实录
4.1 为什么我的程序输出了“Isosceles triangle”但没有“Equilateral triangle”?
这是典型的把两个独立if写成了else if。当你输入3 3 3时,满足a == b,所以第一个if输出等腰。接着是else if,由于上一个if已经执行了,编译器认为整个语句块结束,不会再判断a == b && b == c,从而漏掉了等边输出。解决办法就是把第二个if前面的else去掉,让它们并列。
代码对比:
错误版本:
if (a == b || b == c) { cout << "Isosceles triangle" << endl; } else if (a == b && b == c) { cout << "Equilateral triangle" << endl; }正确版本上面已经给过,关键在于else的缺失。
4.2 为什么我的“Not triangle”一直和后面的输出一起出现?
原因可能是你使用了多个if而没有互相排斥。比如你写了:
if (a + b <= c) { cout << "Not triangle" << endl; } if (a + b > c) { // 继续判断... }这样其实也能工作,但如果你忘了用else或者return,程序在输出Not triangle后还会继续往下执行后面的判断,导致多输出几行。最简单的修复方案是在输出Not triangle后直接return 0;(或exit()),提前终止程序,后面的代码都不执行。这比加else包一层更直观。
4.3 浮点数比较时为什么“a² + b² == c²”总是False?
如果你做的变式题输入是浮点数,那么直接判等十有八九失败。比如1.0、1.0、1.41421356这组数据,1.41421356的平方和2的误差可能在1e-8级别。这时候要用绝对误差判断:
#include <cmath> if (fabs(a * a + b * b - c * c) < 1e-6) { cout << "Right triangle" << endl; }注意:1e-6这个阈值的选取要合适。太小可能把精度误差算成不相等,太大则会把本应是锐角或钝角的三角形误判为直角。通常1e-6在竞赛中可以接受。如果题目保证输入是整数,直接用==即可,避免浮点误差的烦恼。
4.4 为什么排序后a、b、c变了,但后面判断之前没有更新?
如果你是用三个单独的变量而不是数组:
int a, b, c; cin >> a >> b >> c; // 排序 if (a > b) { int t = a; a = b; b = t; } if (b > c) { int t = b; b = c; c = t; } if (a > b) { int t = a; a = b; b = t; }这里的交换逻辑初学者容易写错。比如忘了第三个交换,就会导致排序不完整。建议你使用数组加sort,避免手写交换出幺蛾子。如果排序不对,后面所有的判断都会基于错误的“最大边”,结果自然全错。我调试这类题时,最喜欢在排序后加一句cout << a << " " << b << " " << c << endl;来验证,确认排序无误后再继续。你在本地调试时可以这样做,但提交前记得删掉调试输出。
4.5 等边三角形的角度分类到底是什么?
有些同学会困惑:等边三角形三个角都是60度,所以它属于锐角三角形吗?是的,60度小于90度,所以它是锐角三角形。因此在角分类里,等边三角形会走到最后的else分支输出“Acute triangle”。这道题里,输入3 3 3,预期的输出是:
Acute triangle Isosceles triangle Equilateral triangle如果少了任何一行,就说明分支逻辑还有问题。
4.6 用dev-C++测试明明正确,为什么提交洛谷是WA?
洛谷用的是Linux系统,编译环境和本地Windows可能存在细微差别,但更常见的原因是:本地测试的用例太单一,没有覆盖边界情况。比如你只测试了3 4 5、6 8 10、5 5 5,却漏了1 1 2这种不能构成三角形的数据,也漏了2 2 3这种等腰锐角三角形。建议把下面这些测试用例全部跑一遍:
| 输入 | 预期输出 |
|---|---|
| 1 1 1 | Acute triangle, Isosceles triangle, Equilateral triangle |
| 3 4 5 | Right triangle |
| 2 2 3 | Acute triangle, Isosceles triangle |
| 3 3 5 | Obtuse triangle, Isosceles triangle |
| 1 1 2 | Not triangle |
| 5 12 13 | Right triangle |
| 2 2 2 | Acute triangle, Isosceles triangle, Equilateral triangle |
| 1 2 3 | Not triangle |
| 4 4 7 | Obtuse triangle, Isosceles triangle |
| 6 8 10 | Right triangle |
这些用例基本覆盖了所有分支组合。批量测试是一个很好的做题习惯,能让你的程序和单点思维彻底告别。
4.7 为什么我的程序在顺序上先输出了“Isosceles”再输出了“Acute”?
题目要求的输出顺序是先角后边,也就是如果同时是锐角三角形和等腰三角形,应该先输出“Acute triangle”,再输出“Isosceles triangle”。如果你的代码把边的判断放在角的判断前面,输出顺序就反了,会WA。这一点在扫描题目时很容易被忽略。一定要仔细看题面示例,或者根据输出描述的顺序调整代码。我们的代码把角的判断写在前面,边上两个if写在后面,就是基于这个要求。
5. 从这道题延伸到通用编程能力
5.1 条件分支的排列艺术
P5717是一个特别好的条件分支练习,因为它的分类逻辑不是线性的,而是先有一个前置条件(能否构成),然后有两个维度的分类(角、边),维度之间不完全独立。这种结构在现实业务代码中也很常见,比如判断一个订单的状态是否有效,既要判断状态码,又要判断时间范围。学会用“提前返回”或“分层判断”来组织代码,能让逻辑清晰很多。
我的建议是:遇到分类讨论题,先画一个简单的逻辑树,把互斥分支和并列分支画清楚。互斥分支用if-else if,并列分支用多个if。就像这道题,角分类三个分支互斥,边分类两个分支有包含关系,于是用一前一后交错实现。如果你能在草稿纸上把这个逻辑树画对,代码基本不会错。
5.2 排序预处理:让问题简化一半
这道题最终AC的关键,其实是那个sort(sides, sides + 3)。排序后,“最大边”一目了然,判断三角形构成的复杂度从三次比较降为一次比较。这种“先排序再处理”的模式在很多算法中都有体现:
- 在求中位数、众数时,先排序。
- 在贪心算法中,经常先按某种规则排序,比如活动安排、部分背包。
- 在双指针算法中,排序后可以移动左右指针来逼近目标值。
所以,在解题时,如果发现需要反复比较几个数的大小,不妨想想能不能先排序。多花几行代码,换来的是大量重复逻辑的消除,性价比极高。
5.3 测试用例设计:不要只测“正常情况”
很多新手的代码“看起来对”,但交上去就是WA,根本原因是测试覆盖不全。P5717这个题,我见过有人只测了直角三角形和等边三角形,漏了“不能构成三角形”和“钝角等腰三角形”。设计测试用例时,要尽量覆盖每一个分支:
- 每个输出字符串至少出现一次。
- 边界值:两边之和刚刚等于第三边。
- 特殊形状:等边、等腰直角(比如1 1 根号2,整型没有,但可以构造45度等腰直角,比如输入整数边长不行,放弃)。
- 排列顺序:输入顺序打乱,比如5 3 4,确认排序逻辑有效。
把上面的测试用例表拿去跑一遍,AC就是十拿九稳的事。
5.4 从入门题到竞赛技巧:多写几行防御性代码
竞赛中有一个常见策略:能不用浮点就不用浮点。这道题用整数平方和就能精确判断直角,就不要去开根号、算角度。另外,能用排序简化就不要手写一堆if。能提前return就绝不加大括号嵌套。这些小习惯,能让你在更大规模的题目中减少出错概率。
我还喜欢在写代码前先定义好每个输出字符串的常量,或者直接在cout里写原样句子,确保拼写正确。有的选手喜欢用宏定义字符串,减少重复输入。但这里其实只有一次输出,直接写就够了。
6. 一些额外的想法与个人经验
刷题刷多了以后,你会发现很多“送分题”其实并不那么“送分”,它们隐藏着一些值得反复咀嚼的设计。P5717就是一个典型:它把几何数学和编程逻辑编排得恰到好处,既不会让你觉得高不可攀,也不是无脑输出结果。它考察的正是程序员的基本功——把现实世界的分类规则,翻译成计算机语言。
我自己的体验是:第一次写这道题时,因为没考虑“等腰和等边并列输出”,反复WA了三次。当时我甚至怀疑题库有问题,后来打印出中间结果才发现,3 3 3的输入,我的程序只输出两行。那一刻才真正理解“if和else if”的区别。这种东西光看书是学不会的,必须亲手摔一跤。
如果你现在正处于“看着题解能懂,自己写却总是差一点”的状态,我建议你先把我的代码自己敲一遍,然后遮住代码,重新从零写。写完之后用我给出的测试用例表逐一验证。这个过程可能花不了二十分钟,但它能帮你真正掌握条件分支的精髓。千万不要直接复制粘贴提交,那样你只是完成了一道题,而没有收获一种思考方式。
最后再分享一个小技巧:以后遇到任何需要多条件分类的题,都先在注释里把逻辑树写出来,再对着注释写代码。比如这道题,我会写:
// 排序 // 如果不能构成三角形 -> Not triangle,结束 // 按角分三类(互斥) // 按边分两类(并列)把注释变成代码,你的思路就会清晰得多。这个习惯我跟很多人推荐过,实测非常管用。希望这篇拆解能帮你顺利AC这道题,也让你对“分类讨论”这四个字有更深的理解。