UVa 143 Orchard Trees 是一道让我印象很深的计算几何入门题。当时我刷了一晚上“暴力模拟”类型的题,以为这题也就是枚举坐标点再判断一遍,结果连续提交了四发都 WA,最后才发现自己把题目里的“一棵树”理解成了“一个点”。这道题非常适合刚开始接触计算几何的人,或者准备算法竞赛想在几何模板上补基础的人。题面本身很短:给出一个三角形的三个顶点坐标,顶点坐标是浮点数,数值范围大概在 0 到 100 之间,要求统计这个三角形里到底覆盖了多少棵果树。真正麻烦的地方在于,果树不是一个点,而是一个占据单位面积的正方形。如果不把这个语义吃透,后面再怎么写判断函数都会绕进坑里。
1. 题意还原:果树占的是单位方块,不是树心
1.1 题面里那个不起眼的单位正方形
题目背景很有趣:果园是一个 100 × 100 的矩形区域,横纵坐标可以理解成从 0 到 100 划分成了 10000 个格子位置。果树种在整数坐标点,比如坐标是 (3, 5),那这棵树指的不是一根树干,而是从 (3, 5) 这个点延伸出去、覆盖 [3, 4] × [5, 6] 一整块单位面积的正方形。题意让你判断一棵树是否在三角形范围内,其实是在问:这个单位正方形是不是完全落在三角形内部,或者落在三角形的边界上。
很多人第一次看题会默认成“树心”判断,也就是只检查每个整数点 (x, y) 是否在三角形里。如果题目数据全是普通三角形,这种算法可能样例都能过,但一上正式数据就会挂。原因也很简单:三角形不是只能“尖尖地”覆盖某个点,它还可能斜着切过某个方格。正方形中心落在三角形里,不代表正方形四个角都落在三角形里;只要有一个角伸到三角形外部,这棵树就不能被算作“完全在果园范围内”。
1.2 四角都在内部,为什么就能代表整棵树
这里有一个关键性质:三角形是凸多边形。如果正方形四个顶点都落在某个凸多边形内部,那么正方形的四条边也一定不会穿出这个凸多边形。因为凸多边形的任意两点连线都在图形内部,正方形顶点互相连线形成的边不会跑到外部去。把正方形内部再拆开来看,它由许多水平线和竖直线组成,而这些线的端点通常都在边界上,所以只要顶点都在凸多边形里,整块正方形就不会露头。
这个性质并不是对任何图形都成立。如果是一个 L 形多边形或者月牙形区域,四个顶点都在内部,正方形中间仍然可能横跨外部区域。但三角形没有这种“凹进去”的地方,所以这道题的判定可以简化为:对每个果树方格,判断它的四个角是否全部在三角形内。一个格子的四个角可以取为:
- 左下角
(x, y) - 右下角
(x + 1, y) - 左上角
(x, y + 1) - 右上角
(x + 1, y + 1)
四个角都满足,答案加一。
1.3 用斜边三角形验证“中心点判断”错在哪
我自己测试时常用来暴露问题的一组数据是顶点 (0, 0)、(10, 0)、(0, 10) 组成的直角三角形,斜边是 x + y = 10。如果一棵果树左下角是 (i, j),那么这个正方形最远离原点的角是右上角 (i + 1, j + 1)。要让这个角不超过斜边,需要满足:
code复制(i + 1) + (j + 1) <= 10
也就是 i + j <= 8。满足这个条件的格子数量,逐个累加是 9 + 8 + 7 + ... + 1 = 45。
但如果按中心点判断,只检查 i + 0.5 + j + 0.5 <= 10,得到的是 i + j <= 9,会数出 55 个格子。多出来的 10 个格子里,有的是右上角已经越过斜边,有的是右下角刚好在临界线上但整个方块仍然有一部分在外面。这道题就是靠这些边界格子区分一个选手到底有没有真正理解题意的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 点在三角形内判断:先理解叉积再抄模板
2.1 叉积在几何判断中到底做了什么
既然要判断点是否在三角形内,绕不开的就是叉积。很多人一看到叉积就害怕,其实可以当作一个“方向差”来理解。
假设平面上有向量 OA 和向量 OB,叉积的数值定义为:
code复制cross = (A.x - O.x) * (B.y - O.y) - (A.y - O.y) * (B.x - O.x)
如果结果大于 0,说明从 OA 转到 OB 是逆时针方向;结果小于 0,说明是顺时针方向;结果等于 0,说明两个向量在同一条直线上,也就是三点共线。
对于点是否在三角形内,有一个标准的“同侧法”:把三角形的三条边按顺序走一遍,例如从 A -> B -> C -> A。检测一个点 P 时,分别计算:
code复制叉积1 = cross(A, B, P)
叉积2 = cross(B, C, P)
叉积3 = cross(C, A, P)
如果这三个值都大于等于 0,或者都小于等于 0,说明点始终在路径的同一侧,点就在三角形内部或边界上;如果出现一正一负,点在某条边的外侧,就不在三角形内。这个解释比背公式好记多了,以后遇到“点在多边形内”也可以沿用同一套思想。
2.2 一个不需要调整顶点顺序的判定函数
实际写代码时,经常有人默认输入给定的三个顶点就是逆时针顺序。但 OJ 数据并不会保证这一点,它可能给你顺时针排序的三个点。如果代码写死了“三者叉积都要大于等于 0”,遇到顺时针输入就会把整个判定结果反过来。
所以更稳妥的做法是不要假设方向,而是判断“正负是否混在一起”。只要这三个叉积不是既有正又有负,就认为点在三角形内:
cpp复制#include <cstdio>
#include <cmath>
struct Point {
double x, y;
Point(double x_ = 0, double y_ = 0) : x(x_), y(y_) {}
};
struct Triangle {
Point a, b, c;
};
const double EPS = 1e-9;
double cross(const Point &o, const Point &p, const Point &q) {
return (p.x - o.x) * (q.y - o.y)
- (p.y - o.y) * (q.x - o.x);
}
bool insideTriangle(const Triangle &t, const Point &p) {
double v1 = cross(t.a, t.b, p);
double v2 = cross(t.b, t.c, p);
double v3 = cross(t.c, t.a, p);
bool hasPositive = (v1 > EPS) || (v2 > EPS) || (v3 > EPS);
bool hasNegative = (v1 < -EPS) || (v2 < -EPS) || (v3 < -EPS);
return !(hasPositive && hasNegative);
}
这版代码不关心三角形顶点是按顺时针还是逆时针给出的。对正方形每个角调用一下 insideTriangle,如果四个角都返回 true,就说明这个单位方块完整落在三角形范围内。
2.3 EPS 为什么要存在,又不能设得太大
浮点数比较最忌讳直接写 == 0.0。三角形边界上的点经过乘法和减法之后,叉积理论上是 0,但在 double 里可能是 9.99e-13 这种微小噪声。所以判定时要留一个容差范围,也就是 EPS。
对于这道题,顶点坐标范围不超过 100,叉积结果量级最多到 100 * 100 = 10000,double 的计算误差通常在 1e-12 级别。取 EPS = 1e-9 已经很稳。我看过有些代码把 EPS 设成 1e-3,那问题就大了。三角形边上还会有一圈薄薄的区域被误判成内部,几个本来只有边界擦过的方格会被多算。做几何题时,EPS 的取值会影响答案,尤其是落在边界附近的格子,所以尽量选一个比浮点误差大一到两个数量级、但又不能大到改变实际覆盖关系的值。
3. 我的 WA 排坑记录:从 4 次提交到 1 次 AC
3.1 第一个版本:把树当成整数点
我第一次实现的版本非常简单:读取坐标后,枚举所有整数点 (x, y),判断这个点是否在三角形里。样例通过之后我甚至觉得题目太简单,直到自己构造了一个 1 × 1 小三角形测试。
用顶点 (0,0)、(1,0)、(0,1) 组成一个面积为 0.5 的三角形时,真正的答案是 0,因为面积连一个单位方块都占不满。但如果你检查整数点,(0,0) 在三角形内,程序就会输出 1。这种差异在样例里可能不明显,但题目设计者特意把所有树定义成单位面积方块,就是在筛掉这种“表面正确”的解法。
后来排查这类问题的通用办法是:先造一个小三角形,让斜边正好切过一个格子的右上角,把每个格子的四个角输出出来看。你能直观看到哪个角在外面,比反复猜数据高效得多。
3.2 第二个版本:逆时针假设被数据击穿
修正成“检查四个角是否都在三角形内”之后,我继续沿用了一个经典模板,也就是假设三角形输入是逆时针方向:
cpp复制// 这是错误示范,不能直接用
return v1 >= 0 && v2 >= 0 && v3 >= 0;
这个写法在样例和自己造的逆时针数据上没有任何问题,但提交就是 WA。原因是我忽略了一个事实:输入数据可能把这三个点按顺时针给出。顺时针时,内部点对应的三个叉积全是小于等于 0,而不是大于等于 0。如果只是固定判断非负,真正的内部点反而会被判定成外部。
最简单的修正就是把判断条件改成“三个值全部同号”,或者像前面 insideTriangle 实现那样,遇到正负混合才认为是外部,这样不管输入方向如何都能得到正确结果。
3.3 第三个隐藏坑:输出宽度是 4 位右对齐
第四个版本的几何逻辑已经正确,手测的小样例也全对,提交依然 WA。这就比较让人抓狂了。后来我重新读题才发现,题目明确要求输出一个“占 4 个字符宽度、右对齐”的数字。比如答案是 1,应该输出三个空格加一个 1;如果我直接 printf("%d\n", ans),样例会因为答案比较大显示不出区别,碰巧我又没有专门去测 1 这种小答案,就漏掉了格式坑。
这类老 OJ 题目对输出格式的检查非常严格。解决方法很简单:输出时用 printf("%4d\n", ans);。排查时也可以故意构造一个答案为 0 或 1 的用例,肉眼检查空格数量,一次性暴露问题。
4. 最终能稳定 AC 的 C++ 实现与手算验证
4.1 完整代码
下面的版本兼容 C++98 之后的所有编译器,UVA 老平台也能正常编译。核心思路就是前面说的:枚举所有可能包含果树的整数坐标,对每个单位方块检查四个角。
cpp复制#include <c
