☰
三角形分类题全解析:从条件判断到在线判题的边界与调试实践
2026/10/19 17:15:21 网站建设 项目流程

如果你刷过在线判题平台,大概率见过这种题:给三个整数,让你判断它们能组成什么三角形。题目编号可能不一样,我做的这道叫“1018 - 三角形类别”,要求很简单,逻辑也不复杂,但奇怪的是,每次提交总有人在这里栽跟头。而且栽不是栽在不会写,是栽在“以为自己写对了”。

这道题本身适合刚接触算法题的初学者、准备竞赛基础题的选手,也适合想快速回顾条件判断设计的开发者。它表面上在考“三角形分类”,实际上在考三件事:你能不能把几何定义翻译成严谨的程序条件,你懂不懂边界值测试,你会不会设计没有歧义的输出逻辑。这篇文章我尽量把这三件事拆开讲透,顺带把我自己踩过的坑和我在判题系统里试出来的经验一并分享出来。

1. 这道题到底在考什么:先看懂题目背后的话

1.1 标准的题目描述长什么样

“1018 - 三角形类别”这类题,常见的题干是这样的:输入三个正整数 a、b、c,表示三条线段的长度,要求判断这三条线段能否构成三角形。如果能,进一步判断它是等边三角形、等腰三角形还是不等边三角形。部分变体还要求额外判断直角三角形、钝角三角形和锐角三角形。

输出一般也有明确约定,常见的有三种形态:

  • 输出英文单词,如Equilateral、Isosceles、Scalene、Not a triangle;
  • 输出中文词,如“等边三角形”“等腰三角形”“不等边三角形”“不能构成三角形”;
  • 输出一个整数或字符编码,比如 1 代表不能构成、2 代表等边、3 代表等腰、4 代表不等边。

我这次处理的版本是第一种,输出英文短语。这里我额外提醒一句:输出格式永远比你想的更严格。很多人主逻辑写对了,最后还是 Wrong Answer,原因就是多打了一个空格,或者大小写不对,又或者把Not a triangle写成了Not A Triangle。这类问题在平台上统一叫“Presentation Error”或直接算 WA,反正不会给你过。

1.2 我把题目拆成的四个能力点

这个题在我眼里不是一道“简单题”,而是一道“必须拿满分的基础题”。我每次给学生讲,都会把它的考核点拆成四层:

第一层是理解几何约束,也就是三角形两边之和大于第三边、两边之差小于第三边;第二层是设计判断顺序,先判什么、后判什么,直接决定代码会不会在某些特殊输入上出错;第三层是处理等边和等腰的包含关系,因为从数学定义上说,等边三角形也是一种特殊的等腰三角形,但从题目输出约定来看,很多时候要单独输出“等边”而不是“等腰”;第四层是对输入数据做假设,题目说三个正整数,那要不要考虑 0 和负数?

这四个能力点如果都能在一个小题里过关,后面做更复杂的几何题、排序题、搜索题时,很多基础习惯就已经成型了。所以我一直觉得,这类题目看起来只值一两个测试点,实际上它是判题系统在替你检查“你会不会写工业级条件判断”。

1.3 输入格式里藏着的小陷阱

很多平台的输入是一行三个数,用空格分隔,像3 4 5。但也有的平台是多组测试数据,直到读到文件末尾才结束,也就是大家常说的“EOF 判断”。还有些题目,第一行会给一个整数 T,表示后面有 T 组测试数据。

这三种输入格式对应完全不同的代码骨架:

# 方式一:固定单组输入 a, b, c = map(int, input().split()) # 方式二:不知道几组,读到文件结束 import sys for line in sys.stdin: a, b, c = map(int, line.split()) # 方式三:第一行是组数 T t = int(input()) for _ in range(t): a, b, c = map(int, input().split())

如果你不看清楚题目要求,直接套用自己习惯的输入模板,最常见的结果就是“明明在自己电脑上跑得好好的,一提交就超时或者 WA”。我之前遇到过一个人,题目明确要求多组输入,他非要用input()读单组,第一组跑完程序就结束了,后面的测试数据根本没机会输出。这种错误和算法水平无关,纯粹是读题习惯的问题。

2. 三角形不等式:第一关就在这里翻车

2.1 三条线段构成三角形的充要条件

从几何上讲,三条线段能围成三角形,需要满足一个核心条件:任意两边之和大于第三边。反过来,任意两边之差小于第三边也是等价的。

写成完整条件就是:

a + b > c a + c > b b + c > a

三个条件必须同时成立。

这里我强调一下,很多人的简化写法是“判断最长边小于另外两边之和”,这个思路本身没错,但实现时有个前置动作:你得先找出哪条边最长。如果你不找最长边,直接做三条判断,其实也完全正确,而且不用排序。我个人的建议是,在不确定题目会不会给乱序数据的情况下,直接三条判断最稳妥,代码量也不大。

2.2 常见的两种错误写法

第一个错误写法是漏条件。有人觉得a + b > c就够了,理由是“如果两条短边加起来都大于最长边了,那肯定能构成”。这个理由逻辑上是对的,但问题在于:你并不知道哪条边是短边,哪条是长边。如果输入恰好把最长边放在第一位,比如5 3 3,你判断a + b > c其实是在判断5 + 3 > 3,这个条件当然成立,但它体现不了“两条短边之和大于最长边”。

第二个错误写法是排序之后只判断一次。比如你先把三条边排好序,得到x <= y <= z,然后只判断x + y > z。这个写法的确是正确的,因为其他两个不等式在这种情况下自动成立。但这里有个隐藏的坑:如果你排序写错了,比如排成x <= z <= y,那整个判断就是错的。排序本身也会引入额外的代码量,对要求极简的题解来说反而不划算。

2.3 等于 0 和负数怎么处理

题干明明白白写了“三个正整数”,可实际测试数据里真的会出现 0 或负数吗?我的经验是:绝大概率不会,但你最好还是防一下。

为什么说要防?因为不防的情况下,0 和负数会导致一些诡异的输出。比如输入0 0 0,如果你只做大于判断,0 + 0 > 0不成立,会被判为不能构成三角形,这倒还能接受。但输入1 2 -3这种负数,三个和判断全都不成立,也会落到“不能构成三角形”,看似没问题。真正危险的是-1 3 5,其中-1 + 3 > 5不成立,所以也会被正确排除。

但如果你用的是排序后判断最长边的方式,并且只写了x + y > z,这时候负数和零也大概率不会通过,所以整体风险还是可控的。我之所以建议防御一下,不是为了应付判题数据,而是为了让代码逻辑自洽。我见过有人连“三条边必须都大于 0”都没写,最后在解释代码时被面试官追问得哑口无言。

2.4 我实际写代码时用的是哪套判断

我自己最常用的是这样一段:

def is_valid_triangle(a, b, c): if a <= 0 or b <= 0 or c <= 0: return False return a + b > c and a + c > b and b + c > a

先排除非正数,再用三组不等式做严格判断。这里为什么要写成>而不是>=?因为当两边之和恰好等于第三边时,三条线段会贴在同一条直线上,无法围成平面三角形,这在数学上叫“退化三角形”。题目说“构成三角形”,按大学几何的严格定义,退化的不算。

3. 判断顺序即策略:等边、等腰、不等边的先后关系

3.1 从集合关系理解三类三角形

在几何里,等边三角形、等腰三角形、不等边三角形是有明确包含关系的。等边三角形的三条边相等,等腰三角形有两条边相等,不等边三角形三条边互不相等。严格来说,等边三角形可以看作是“三条边都相等的等腰三角形”,它是等腰三角形的特例。

但判题系统不会跟你讲这些哲学。很多题目的输出约定是:如果能构成三角形,优先判断是不是等边;如果不是等边,再判断是不是等腰;如果都不是,就是不等边。也就是说,输出等边的时候,不再输出等腰。

如果题目不是这种约定,而是要求“是等腰三角形就输出等腰,等边也算等腰”,那判断顺序就完全不同了。你的第一件事永远是读清楚输出要求,而不是按数学直觉来。

3.2 正确的判断顺序长这样

我推荐的顺序是最经典的三步:

def classify_triangle(a, b, c): if not is_valid_triangle(a, b, c): return "Not a triangle" if a == b == c: return "Equilateral" if a == b or a == c or b == c: return "Isosceles" return "Scalene"

这段代码的核心逻辑是:先保证它是一个合法三角形,再判断三边相等,再判断是否有任意两边相等,最后落到不等边。

这里每一步的顺序都有讲究。先判合法性,是因为一个“两边相等”的三元组可能根本成不了三角形,比如1 1 3,两边相等满足等腰条件,但1 + 1 > 3不成立,它压根不是三角形。如果先判等腰,会输出一个错误的等腰三角形结果。所以合法性判断必须放在最前面。

先判等边,再判等腰,是因为等边三角形的三边相等,它同时满足a == b、b == c、a == c,如果你把等腰判断放在前面,等边三角形会被归为等腰,输出就错了。所以要先用等边判断把三边相等的情况“截胡”。

3.3 如果题目要判断直角三角形怎么办

有些变体题不满足于只分三类,还会让你在等腰、等边之外额外区分直角三角形、锐角三角形、钝角三角形。这种题目在输出时经常会有一个优先级:比如“如果是等腰直角三角形,优先输出等腰直角”。

这时候判断顺序会变成这样:

  1. 先判能否构成三角形;
  2. 再找出最长边;
  3. 用勾股定理判断是否直角三角形;
  4. 如果既是等腰又是直角,按题目要求输出“Isosceles Right Triangle”之类;
  5. 否则分别走等边、等腰、不等边的分类。

这里最关键的坑是浮点数精度。你输入的是整数边长,但勾股定理判断要用到平方和,例如a^2 + b^2 == c^2。如果三条边都很长,比如几百万级别,平方以后的结果很可能超过 32 位整数的上限,导致溢出。用 Python 这种大整数语言还好,用 C 或 Java 的 int 就会出问题。我的应对方案是:要么把边长读成 long long,要么把平方比较拆开写成乘法而不是直接处理结果,要么用等价的边长比较法,例如先排序再判断x*x + y*y == z*z,同时确认 x、y、z 都是整数可安全相乘。

3.4 一个容易被忽略的边界:1 1 1

很多初学者拿1 1 1测试,程序输出等边三角形,松了口气。但他们在测试1 1 2时突然懵了——两边之和等于第三边,输出“不能构成三角形”。这个结果是对的,但在他们脑子里,图形上是“两条短边贴在一起趴在长边上”的样子,确实不像个三角形。

这就是我常说的:判断顺序的根,在于你对“三角形”的定义是否严格。如果定义里允许退化三角形,那1 1 2也应该能构成一个面积为 0 的三角形。不过绝大多数竞赛题默认不允许。所以你记住一句话:题目说三角形,默认就是不允许退化。

4. 一份经得起折腾的代码模板:从 Python 到 C 的双版本

4.1 Python 版本:短小清晰,也适合快速拍错

我用的主版本是 Python 3,完整代码如下:

import sys def is_valid_triangle(a, b, c): if a <= 0 or b <= 0 or c <= 0: return False return a + b > c and a + c > b and b + c > a def classify_triangle(a, b, c): if not is_valid_triangle(a, b, c): return "Not a triangle" if a == b == c: return "Equilateral" if a == b or a == c or b == c: return "Isosceles" return "Scalene" for line in sys.stdin: line = line.strip() if not line: continue a, b, c = map(int, line.split()) print(classify_triangle(a, b, c))

这段代码里值得注意的地方有三个。第一,我用了sys.stdin循环,能处理多组输入;第二,我过滤了空行,防止测试文件里有奇怪的换行符;第三,分类函数拆得很干净,方便单独测。

如果你的题目是第一种“T 组输入”格式,只需要把主体改成:

t = int(input()) for _ in range(t): a, b, c = map(int, input().split()) print(classify_triangle(a, b, c))

我个人会比较喜欢用函数封装逻辑,这样即使后面题目升级成“还要输出周长”,也不用大改主流程。

4.2 C 语言版本:注意输入缓冲和输出匹配

用 C 做题时,我一般写成这样:

#include <stdio.h> int is_valid(int a, int b, int c) { if (a <= 0 || b <= 0 || c <= 0) return 0; if (a + b > c && a + c > b && b + c > a) return 1; return 0; } int main() { int a, b, c; while (scanf("%d %d %d", &a, &b, &c) != EOF) { if (!is_valid(a, b, c)) { printf("Not a triangle\n"); continue; } if (a == b && b == c) { printf("Equilateral\n"); } else if (a == b || a == c || b == c) { printf("Isosceles\n"); } else { printf("Scalene\n"); } } return 0; }

C 版本让我最头疼的是两件事。第一是while (scanf(...) != EOF)这种写法,它要求输入文件确实以 EOF 结尾,而 Windows 下手工做测试时,你可能需要按 Ctrl+Z 来模拟 EOF,不然程序卡在读取状态。第二是输出换行,判题系统非常在意每一行的结尾是不是\n,你少打一个就容易被判 Presentation Error。

如果你是在本地用命令行测试,我建议准备一个test.txt文件,把多次输入写在里面,然后运行./a.out < test.txt,这样最接近判题环境。

4.3 Java 版本:主类名和读取方式两个大坑

Java 版本我写得不多,但每次帮人调试都会遇到两个常规问题。一个是主类必须叫Main,另一个是用Scanner还是BufferedReader的选择。

import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); while (sc.hasNextInt()) { int a = sc.nextInt(); int b = sc.nextInt(); int c = sc.nextInt(); if (!isValid(a, b, c)) { System.out.println("Not a triangle"); continue; } if (a == b && b == c) { System.out.println("Equilateral"); } else if (a == b || a == c || b == c) { System.out.println("Isosceles"); } else { System.out.println("Scalene"); } } sc.close(); } static boolean isValid(int a, int b, int c) { if (a <= 0 || b <= 0 || c <= 0) return false; return (long)a + b > c && (long)a + c > b && (long)b + c > a; } }

在isValid里,我把a + b强转成long,是因为当 a 和 b 都是int上限附近的值时,它们的和会溢出成负数,进而导致大于比较完全失效。这是 Java 和 C 语言里特别容易被忽略的点,Python 的大整数特性掩盖了这个问题,让很多 Python 选手转到 C/Java 后水土不服。

4.4 为什么我建议你三个语言都写一遍

写这个题的时候,我特别建议初学者把 Python、C、Java 三个版本都写一遍。这不是在卷语言数量,而是因为每个语言会逼你思考不同的东西:Python 让你关注逻辑顺序,C 让你关注类型溢出和 EOF,Java 让你关注类名和包结构。

这三种关注点加在一起,基本上覆盖了竞赛题目中 80% 的非算法性陷阱。等你把这些小坑都踩过一遍,后面做复杂题时就不用战战兢兢地同时维护“算法思路”和“语言细节”了。

5. 测试驱动:用边界值和极端输入把答案钉死

5.1 最基础的测试矩阵

我每次写完代码,不会急着提交,而是先用一组覆盖所有类别的小样本跑一遍。下面这张表是我自己常用的测试矩阵,你可以直接复制去测:

输入 (a, b, c)期望输出测试意图
2 2 2Equilateral等边三角形,三边相等
3 3 5Isosceles等腰三角形,底边为 5
3 5 3Isosceles等腰三角形,打乱顺序
5 3 3Isosceles等腰三角形,最长边放第一位
3 4 5Scalene直角三角形,但按普通分类是不等边
1 2 3Not a triangle退化情况,两边之和等于第三边
1 3 1Not a triangle两边之和小于第三边
1 1 3Not a triangle看似等腰但构不成三角形
0 0 0Not a triangle非法输入,全零
-1 2 3Not a triangle非法输入,含负数

这张表跑完,你的核心逻辑基本就是稳的。注意我特意加入了3 5 3和5 3 3这种输入,它们考验的是你的代码是否对输入顺序不敏感。如果你在排序或者判断条件时默认了“第一个数是短边”,很可能在这类输入上出错。

5.2 更狠的边界:大数、极小数、多组混合

如果你想把代码压榨到极限,我这还有一组进阶数据:

  • 1000000000 1000000000 1000000000:三条大数相等,验证是否会溢出;
  • 1 999999999 1000000000:接近退化的长线段,验证不等式判断是否严谨;
  • 多组数据连续输入,中间不隔空行;
  • 最后一行末尾没有换行,验证你的循环读取方式是否可靠。

这组数据我一般不在第一次提交前测,因为大概率会暴露语言层面的溢出问题。但测过之后,代码的鲁棒性会明显上一个台阶。比如 Python 版本会轻松通过,C 和 Java 版本如果在和判断时没有转 long,就可能在这组数据上翻车。

5.3 在线判题平台的“黑盒”心态

在线判题不会告诉你它测了哪些数据,你只能看到 WA、RE、TLE 这类结果。所以你必须假设它能测到所有极端的输入。有一次我在某个平台上提交 C 代码,明明本地全过,结果一交就 WA。后来我发现,平台的数据里有一条足足 20 个字符的超长数字,我的scanf("%d")虽然能读进去,但那条数据本身可能已经超出 int 上限,导致整个三元组乱掉。

从那以后,我养成了一个习惯:只要题目说输入是整数,我就默认它可能超过 2^31-1,要么用 long long,要么用 Python。这类题目的数据量不大,用 long long 完全不会牺牲性能,但能省掉一大堆调试时间。

5.4 测试代码本身也别写崩

写测试脚本时,很多人喜欢直接手敲数据一遍遍跑。我的做法是写一个简单的生成器,把非法输入、边界输入、等价类输入都自动组合起来跑一遍。比如:

import itertools import random edges = [0, 1, 2, 3, 4, 5, 999999999, 1000000000] for a, b, c in itertools.product(edges, repeat=3): result = classify_triangle(a, b, c) # 这里可以加断言,对比几何判定结果

这样能在一分钟内生成成千上万组测试组合,远远覆盖手工测试的范围。虽然小题没必要这么隆重,但我把这个当成练手的机会,毕竟以后做复杂项目时,这种自动化测试思路是能直接迁移的。

6. 常见提交错误与调试技巧:从 WA 和 RE 里找到真相

6.1 排名靠前的 WA 原因

根据我带过的人的经验,这道题 WA 的原因大致可以归为五类:

第一类是判断顺序错误,把等腰判断放在了等边前面,导致等边三角形被输出成等腰;第二类是三角形合法性判断缺失,对1 1 3这种数据输出了等腰三角形;第三类是输出格式错误,大小写写错、多打空格、少打换行,或者把 “Not a triangle” 写成了 “Not triangle”;第四类是输入处理错误,把多组输入当成单组处理,导致只输出第一组结果;第五类是类型溢出,在 C 或 Java 里用int做加法判断,遇到大数时和变成负数,判断结果反而错掉。

这些原因单独看都很蠢,但它们组合在一起就形成了一个很有趣的现象:你认为最简单的题,往往最容易让你在细节上栽跟头。

6.2 我的三步排查法

如果你提交后 WA 了,我建议按下面三步排查:

第一步,开着本地代码,把所有测试矩阵跑一遍,确认核心逻辑没问题。如果跑出来就错了,说明是算法或条件的问题。

第二步,把输入可能存在的 EOF、多组数据、空行等因素都考虑进来,在你自己的测试文件里模拟好,然后重跑一遍。如果这一遍挂了,说明是输入处理的问题。

第三步,如果前两步都过了还是 WA,那基本可以锁定为输出格式或类型溢出问题。这时候把输出原样复制到一个文本文件里,用十六进制编辑器看一眼有没有隐藏的空格、制表符或 BOM 头。这个习惯看起来很极端,但我确实遇到过因为 UTF-8 BOM 导致判题系统无法识别输出的情况。

6.3 RE 和 TLE 又是什么情况

这道题出现 TLE 的概率极低,因为解法本身是 O(1) 直接算完。如果你 TLE 了,大概率是输入循环写法有问题。比如有人用while True读行,读不到数据时不 break,程序就一直在空转,直到超时。

RE 也就是运行错误,常见于 C 语言数组越界和 Java 的ArrayIndexOutOfBoundsException。这题不涉及数组,RE 的来源大多数是因为scanf读到的类型不匹配,或者String.split()切割后数组长度不足。我之前见过有人用line.split(" ")去切输入,结果数据里连续多个空格,切出来一堆空字符串,转 int 时直接抛异常。用line.split()就能避免这个问题,它会自动按空白字符切分。

6.4 调试技巧:输出调试信息前先想想判题系统的脾气

我特别想强调一点:提交到判题系统的代码里,绝对不能有额外的调试输出。很多人本地调得好好的,提交前忘了删print()调试语句,结果输出里混了一堆多余信息,直接 WA。这个错误低级又高频,我在各个群里几乎每周都能看到有人问“为什么我加了 printf 反而 WA”。

如果你想看中间变量的值,最好在本地跑;如果你想保留调试代码又怕提交时忘删,可以用一个全局开关控制。比如:

DEBUG = False def debug(*args): if DEBUG: print(*args)

提交前把DEBUG改成False,代码不用动。这个小习惯虽然简单,但能救你很多次。

7. 从三角形分类延伸到更通用的思路:面试和工程里的迁移价值

7.1 三步走的决策模式

这种“先排除非法条件,再处理特例,最后落到一般情况”的决策模式,远远不止能用在这道题上。

我举个现实例子:你在写一个会员等级判断系统,输入年龄、消费金额、是否是 VIP,要输出用户等级。逻辑结构和三角形分类完全一样,得先判断输入是否合法,比如年龄不能为负,再判断有没有 VIP 特例,最后根据金额和年龄划分普通等级。如果顺序写反了,一个 -5 岁的 VIP 用户可能被误判成高级会员。

再比如做表单校验:先判必填项是否为空,再判格式是否合法,然后再判字段长度和唯一性约束,最后才是业务规则判断。这就是三角形分类中“合法性优先,特例优先,规则兜底”的延伸。

7.2 单元测试思维从一个小题开始

我在写这个题的小测试矩阵时,其实就在不知不觉地实践单元测试。每一个输入一行,配一个期望输出,就是最早的测试用例。

后来我做工程项目时发现,很多团队的功能测试用例设计得还没有这道题的测试矩阵全面。他们只测了正常输入,不测边界值,不测异常输入,不测顺序变化。如果你可以从这道题开始养成“等值类和边界值”的思维,后面的职业道路会顺很多。

7.3 从不变量看程序正确性

这个题还有一个更深的启发:程序的核心不变量是“分类结果必须和几何定义一致”。代码里写的每一个 if,都必须对应几何上的一条定义或定理。我后来在调很多条件密集型代码时,都会问自己一个问题:我写的这个条件,到底是对应什么物理意义或业务约束?

如果答不上来,那这个条件大概率是多余的或者错误的。这个问题本身就是代码审查的基础方法论。回头再看这道只有十几行代码的小题,它其实把一个代码审查的原则浓缩到了最小规模里。

7.4 以后遇到“1018”这种编号题该怎么准备

最后分享一个实用策略:不要只刷一遍就丢。拿到这种编号题,我建议给自己定四个目标。第一,用至少两种语言写过并且都 AC。第二,用自动化生成器跑一批随机数据,确认逻辑完善。第三,把题目的变体都做一遍,比如带直角三角形、带面积输出、带最长边输出。第四,把判断逻辑封装成函数,方便以后在别的题里复用。

这道题的函数,我已经在多个项目里直接复制改写过。比如一个地理几何工具要判断三个坐标点是否共线,我先用三边分类思路判断它们能否构成三角形,如果不能,就是共线或近似共线。小逻辑的复用价值,往往比你想象得大。

我个人实际写题时还有个很小的习惯:每写一个分类判断题,都会在注释里写明“这个判断对应哪个数学依据”,比如# 三角形不等式:任意两边之和大于第三边。等代码写多了回头看,这些注释就是你的知识图谱。有人说小题不用写注释,但我觉得,能让人在三个月后一眼看懂当时想法的注释,永远不亏。

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

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

立即咨询