1. 从水题到必会题:先说清楚这道题到底在考什么
如果你刷过一段时间的洛谷,一定对“【深基】”这个标签不陌生。这套题目对应的是一本很经典的入门教材的课后练习,P5717这道题排在循环和分支结构的章节末尾,名字叫“三角形分类”,单看题面确实很短:输入三个正整数,判断能否构成三角形;如果能构成,再输出它是锐角三角形、直角三角形还是钝角三角形。
很多新手拿到这题的第一反应是:这有什么好做的?不就是两边之和大于第三边,然后比一下最长边的平方和另外两边平方的关系吗?但真的动手写代码的时候,你会发现事情没那么简单——这题的隐藏考点有两个:一是三条边的排序处理,二是输出条件的优先级顺序。这两个点如果没想清楚,即使你的“核心公式”背得再熟,也会在某个测试点上翻车。
另一个容易被忽略的现实是:这道题在竞赛圈里属于“不上台面”的入门题,但在实际刷题过程中,它是一个非常典型的分支结构综合应用题。它把“读入数据 → 逻辑判断 → 条件组合 → 格式化输出”这四个环节全部串起来了,而且数据范围极简单,不需要任何算法优化,纯粹考察你对 if-else 结构的控制能力。所以我的建议是:别因为它简单就跳过,把这道题吃透,后面再遇到“逻辑判断类”题目,你能省掉大量调试时间。
这道题适合谁来写?刚学完 if / else if / else 语法的新手,准备参加 CSP-J 入门组比赛的选手,以及想复习“三角形判定”这个经典数学模型的开发人员。不需要任何算法基础,会写最基本的比较表达式就能做。
2. 整体思路拆解:为什么“排序”比“判断”更关键
先说一下这道题的输入格式:一行三个正整数,范围是 1 到 10000。这个范围很友好,意味着你不用考虑大数溢出问题,int 类型完全够用,直接做平方运算也不会超过 int 的上限。
拿到三个数以后,第一个直觉是:直接判断两边之和是否大于第三边,然后分类。问题来了——锐角、直角、钝角的判定公式,默认你是用“最长边”作为参照的。比如直角三角形的判定是“两条短边的平方和等于最长边的平方”,如果不知道哪条边最长,你怎么写条件?难道把三种排列组合都写一遍?当然可以,但那样代码会变得非常啰嗦。
所以这个题最核心的思维步骤是:先把三个数排好序,让 a <= b <= c,这样 c 一定是最长边,后续所有判断都围绕 c 展开,逻辑一下子就清晰了。这也是这一类“几何判定型题”的通用套路。
具体判断逻辑分两层:
第一层:判断能否构成三角形。如果不满足,直接输出“Not triangle”。
第二层:在能构成三角形的前提下,用三条边平方之间的关系判断角类型。这里有个新手容易忽略的细节:等边三角形和等腰三角形也要在这个分支里处理,而且它们的输出和角类型并不冲突。比如等边三角形一定是锐角三角形,但它还要额外输出“Equilateral triangle”;等腰直角三角形虽然实际存在(比如两条直角边相等),但在这道题的输出规则里,你要把几种分类都列出来。
我见过不少人把“等腰”和“等边”的判断放在最前面,然后直接 else 掉后面的角判断,结果是等边三角形只输出了等腰和等边,反而漏掉了“锐角三角形”这个结论。这是最常见的一个错误。正确做法是:角类型的判断单独做,边特殊情况的判断也单独做,两者互不干扰,该输出几条就输出几条。
我们来看一下题目的输出约定:
- 如果三边不能构成三角形,输出“Not triangle”。
- 如果能构成:
- 如果是直角三角形,输出“Right triangle”;
- 如果是锐角三角形,输出“Acute triangle”;
- 如果是钝角三角形,输出“Obtuse triangle”;
- 如果是等腰三角形,输出“Isosceles triangle”;
- 如果是等边三角形,输出“Equilateral triangle”。
注意:这些输出没有“互斥”关系,一个三角形可能同时输出多行。比如输入 1 1 1,它会输出:
Acute triangle Isosceles triangle Equilateral triangle所以你在写代码的时候,不能把这些条件写成 if / else if 链,而是要写成多个独立的 if。这是很多新手第一次接触“多重分类输出”这类题型,思路没转过弯来,才会把多个 if 写成互斥的 else if。
3. 核心判定公式与数学原理:勾股定理的三种形态
三角形的角分类,本质上是在比较“最长边的平方”和“另外两条边平方和”的大小关系。这个结论的数学依据就是余弦定理的退化版本。
对任意三角形,设三边从短到长为 a、b、c,夹角 C 是 a 和 b 的夹角,c 是它的对边。余弦定理说:
c² = a² + b² - 2ab·cos(C)
如果 C = 90°,cos(C) = 0,那么 c² = a² + b²,也就是直角三角形的判定条件。
如果 C > 90°(钝角),cos(C) < 0,那么 -2ab·cos(C) > 0,所以 c² > a² + b²。也就是说,最长边的平方大于另外两边平方之和时,这个三角形是钝角三角形。
如果 C < 90°(锐角),cos(C) > 0,那么 -2ab·cos(C) < 0,所以 c² < a² + b²。也就是说,最长边的平方小于另外两边平方之和时,三角形三个角都是锐角,是锐角三角形。
这里要注意,锐角三角形的条件是“最大角小于90°”,而最大角对应的就是最长边。所以只需要比较最长边的平方和另外两边平方和的关系即可,不需要去判断另外两个角。
整理成代码逻辑(假设 a <= b <= c):
- 如果 a + b <= c:输出 Not triangle(不能构成三角形)。
- 否则:
- 如果 c² == a² + b²:输出 Right triangle。
- 如果 c² < a² + b²:输出 Acute triangle。
- 如果 c² > a² + b²:输出 Obtuse triangle。
- 如果 a == b 或 b == c:输出 Isosceles triangle。
- 如果 a == b 且 b == c:输出 Equilateral triangle。
注意等腰三角形的判定条件:只要存在任意两边相等即可。因为我们已经排好序了,所以只需要检查 a == b 或者 b == c。从逻辑上说,如果 a == c,那么一定也有 a == b 和 b == c(因为排序后是连续的),所以不需要单独写 a == c。不过为了代码可读性,有些人也会写成 a == b || b == c || a == c,这是安全的冗余写法。
等边三角形实际上包含了等腰的条件,所以输出顺序上,先判断等腰再判断等边是合理的。当然先判断等边再判断等腰也可以,只要两个都是独立的 if 就行。
4. 代码实现与逐行解读:一份可以直接抄作业的 C++ 版本
这种题目用 C++ 写最直观,因为涉及大量分支判断,C++ 的 if-else 语法非常清晰。我下面给出一个比较精简的写法,然后逐行解释关键部分。
#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]; if (a + b <= c) { cout << "Not triangle" << endl; return 0; } if (c * c == a * a + b * b) { cout << "Right triangle" << endl; } else if (c * c < a * a + b * b) { cout << "Acute triangle" << endl; } else { cout << "Obtuse triangle" << endl; } if (a == b || b == c) { cout << "Isosceles triangle" << endl; } if (a == b && b == c) { cout << "Equilateral triangle" << endl; } return 0; }这段代码里,我用了 STL 的 sort 函数。有些同学可能还没学到 STL,那一开始手写一个三数排序也不难,比如用三次比较交换。但既然题目不给数据范围压力,用 sort 是最清晰、最不出错的选择。如果比赛环境不允许用 STL,或者你还没学到,可以用下面这个替代方案:
// 手写三数交换排序 if (a > b) swap(a, b); if (b > c) { swap(b, c); if (a > b) swap(a, b); }排序结束后,a <= b <= c 这个前提就保证了。接下来判断三角形能否成立,用的是 a + b <= c,这个条件是“不能构成”的充分必要条件。注意这里必须是小于等于,因为等于时三边共线,构不成面。
再看角判断的三个条件。因为最长边是 c,所以只需要关注 c² 与 a²+b² 的大小关系。我这里用了 c * c,因为三个数最大是 10000,c² 最大是 100000000,int 最大约 21亿,完全没有溢出风险。如果你的数据范围更大,比如到 10^9,那就要用 long long,不过这道题不需要。
等腰和等边的判断,我写成两个独立的 if,因为它们和角类型判断互不影响。这里有一个细节:a == b || b == c 已经覆盖了所有等腰的情况——因为 a <= b <= c,如果 a == c,说明三个数都相等,那 b 也必然等于 a,所以 a == b 成立。所以这个写法是安全的。
输出格式方面,每个字符串后面都有 endl(或者 \n),保证每行一个结果。顺序不要改,先角类型,再等腰,再等边。实测这样写能直接 AC。
5. 输出顺序与判题规则:为什么不能把 “Acute” 放在最后
这一节专门聊一个新手很容易踩的坑:输出顺序。
洛谷的 SPJ 对这个题是全文比对?实际上是按行比对的,每一行输出必须和标准答案完全一致。所以输出顺序不能乱。题目描述里给出的顺序是:Right triangle → Acute triangle → Obtuse triangle → Isosceles triangle → Equilateral triangle,但注意“Not triangle”优先,一旦输出这个,后面什么都不用输出。
如果你把等腰三角形判断放在角类型之前,比如先输出 Isosceles triangle,再输出 Acute triangle,那输出顺序就反了。对于 1 1 1 这个测试点,你输出的是:
Isosceles triangle Equilateral triangle Acute triangle标准答案是:
Acute triangle Isosceles triangle Equilateral triangle肉眼看起来内容一样,但判题程序是按行精确比对的,顺序不同就是 Wrong Answer。所以我在写代码的时候,会严格按照题目描述中列出的输出顺序来排列 if 分支。
另外一个小细节:标准答案中,等边三角形一定同时也是等腰三角形,所以等边三角形的输出一定包含两行:Acute triangle 和 Isosceles triangle,再加上一行 Equilateral triangle。如果某位同学把“Isosceles triangle”和“Equilateral triangle”写成了 else if 的关系,那 1 1 1 只会输出:
Acute triangle Isosceles triangle结果就完全错了,丢了等边这一行。这个错误的隐蔽之处在于:你用 3 4 5 这种直角三角形测试的时候完全没问题,因为直角三角形不是等腰三角形,两个判断各自为真,输出正好两条;但一旦你拿 1 1 1 来测,立刻暴露。
这也是我想强调的一个工程习惯:对于可以同时成立的多种状态,永远不要用 else if 把它们串联。这不是这道题独有的坑,在未来做模拟类题目时,你会反复遇到“状态叠加”的问题。想清楚了这点,这一章就不白学。
6. 常见错误与排查技巧:实测最容易翻车的几个点
我让几个学编程的朋友分别写了这个题,收集到了以下高频错误,整理成一张速查表,供你对照自查。
| 错误现象 | 根本原因 | 解决方案 |
|---|---|---|
| 输入 1 1 1 输出缺行 | 等腰和等边用 else if 连接 | 拆成独立的 if |
| 输入 5 5 9 输出 Not triangle | 判定条件写成了 a + b < c | 改为 a + b <= c,等于的情况也不能构成三角形 |
| 输入 3 4 5 输出 Right triangle 后还输出 Acute triangle | 角类型判断用 if 而非 else if 链 | 角类型三选一必须互斥,用 else if 或 switch |
| 输入 6 8 10 输出 Obtuse triangle | 排序没做好,比较时用了错误的“斜边” | 先排序,让最长边统一用 c 表示 |
| 输出顺序与标准答案不同 | 输出顺序没有按照题目列出的顺序 | 严格按照 Right → Acute → Obtuse → Isosceles → Equilateral 排序 |
| 用了 double 计算平方导致精度问题 | float 比较有误差 | 直接用 int 计算平方,边长都是整数 |
第一行的问题在上面已经详细说了。第二行的问题也很典型:有些教材里写“三角形两边之和大于第三边”,于是新手写出来的判断条件是 a + b > c,取反就写成了 a + b < c。但实际上,当 a + b == c 时,三点共线构不成面积,也是不行的。所以不构成三角形的条件必须是 a + b <= c。这一点,建议你在草稿纸上画一条数轴,把等式情况单拎出来,印象会更深。
第三行的问题属于逻辑优先级迷惑。直角三角形、锐角三角形、钝角三角形是三重互斥关系,一个三角形不可能同时是直角和锐角。所以这里必须用 else if,保证最多只会输出一个角类型。而等腰、等边与角类型并不是互斥的,所以要用独立的 if。很多新手把这两层关系混在一起,写出一堆 else if,就出现了“3 4 5”这种直角三角形额外输出“Acute”的情况。
第四行的错误是数学公式没落地。如果你不排序,直接用输入的 a、b、c 去套 c² == a² + b²,当输入顺序是 6 8 10 时没错,但输入 10 8 6 时就会把 10 当作短边,结果判断不成立。解法很简单:排序。这个排序不只是为了公式成立,更是为了让代码逻辑对任意输入顺序都稳定。
第五行的输出顺序问题前面已经专门讲过,这里不再重复。
第六行是一个隐藏问题:有些同学担心乘法溢出,于是把边长转成 double,然后用 sqrt 或者直接浮点比较。实话实说,这道题确实不少人会用浮点,但这是一种很不好的习惯。第一,浮点数比较存在误差,比如 0.1 + 0.2 != 0.3 的经典问题;第二,整数平方比较完全不需要浮点,直接算 c * c 和 a * a + b * b 就完了。用浮点不但没有收益,还引入了不确定性,这个习惯一定要改。
7. 从这道题延伸出去:三角形相关题型的三个变种
P5717 是一个基础中的基础,但它背后的逻辑却可以迁移到很多其他题上。我根据个人刷题经验,总结几个常见的“三角形变形题”,你做完这题之后可以顺带练一练思维。
变种一:判断三角形类型并输出面积。这种题会要求你额外计算面积,常见做法是海伦公式。海伦公式需要先判断能否构成三角形,这刚好就把本题的第一层判断复用过来。计算面积时,因为要考虑开根号的问题,需要用到 double,此时要注意输出精度格式,一般要求保留两位小数。核心逻辑不变,排序思想也一样。
变种二:给你三条边,问能否组成直角三角形(或钝角三角形),但不要求输出具体类型。这种题通常会加大数据范围,比如边长到 10^9,那 c * c 就会超过 int 范围,你必须用 long long。但判定逻辑完全相同,都是“排序 + 平方比大小”。
变种三:给定若干组三条边,统计其中能组成三角形的组数。这种题多了一个循环结构,但内层还是一样:每组读三个数,排序,判 a + b > c。如果你在本题已经熟练掌握了排序的写法,这个变种题就只是套一层 for 循环的事。
你会发现,很多看似不同的题目,底层的“思维定式”就是:先排序、再判定、后输出。把这套思维模式刻进肌肉记忆,比单纯背一道题的代码有用得多。
8. 关于测试数据:自己在本地多跑几组边界值
这道题的测试点虽然不多,但你自己写代码的时候,建议至少手动测以下几组数据:
1 1 1 输出: Acute triangle Isosceles triangle Equilateral triangle注意这一组同时验证了三个独立的 if 是否都有输出,以及输出顺序。
1 2 3 输出: Not triangle这一组验证了 1 + 2 == 3 的边界情况,必须判定为不能构成三角形。
3 4 5 输出: Right triangle直角三角形,又不是等腰,所以只有一行。
5 5 8 输出: Acute triangle Isosceles triangle这里的等腰锐角三角形,验证了输出两行的情况。
2 2 3 输出: Obtuse triangle Isosceles triangle注意 2² + 2² = 8,3² = 9,8 < 9,所以是钝角三角形,同时是等腰三角形。这个数据对验证“钝角 + 等腰”同时输出很有用。
如果你本地把这几组数据全部跑过且输出顺序与标准答案完全一致,这道题基本就稳了。不要只测样例,样例只能帮你验证最基本的逻辑,边界值才能逼出真正的 bug。
9. 个人经验总结:这道题教会我的三件事
刷题时间长了你会发现,越是简单的题,越能暴露基本功问题。P5717 我反复讲过很多遍,每次让新人写,都会有人踩到上面提到的坑。我自己也在一开始写过用 double 判断的版本,后来对比整型方案,才发现自己绕了远路。
第一点体会是:写分支结构题之前,先把所有可能的输出情况在草稿纸上列出来。不要直接开写代码。比如本题,你可以先画一个决策树:是否构成三角形 → 属于哪类角 → 是否等腰 → 是否等边。把这个树画清楚,写代码就变成了翻译,而不是现场编逻辑,错误率能下降一半。
第二点体会是:排序是一种成本极低的“降维打击”手段。数据规模是三个数,排序的时间几乎可以忽略不计,但它带来的逻辑简化是巨大的。很多几何类的题目,你先排序,后续的代码能短一半以上。这个习惯在做复杂题目时尤其重要,因为每一步都少一个分支,整体出错的概率就小很多。
第三点体会是:输出顺序也是得分点。很多 OJ 题目是按行精确匹配,你内容全对但顺序不对,依然是零分。以后遇到任何涉及多行输出的题,第一件事就是确认输出顺序,然后严格照做。
这道题虽然简单,但我建议你把它收藏起来,过两周再写一遍。如果你能一次 AC,说明分支结构真的入门了。如果还出错,那正好借这个机会补一补逻辑顺序上的盲区。刷题本质上是练脑,不是赶进度,每一道题都值得你多花十分钟想清楚“为什么这么做”。