OperationLove - 题解
标签与难度
标签: 计算几何, 叉积, 图形识别, 几何特性, 浮点数处理, 不变量 难度: 1400
题目大意喵~
一位名叫爱丽丝的机器人美女想要通过一个谜题来挑选夫君,真是个浪漫的考验呢,喵~
谜题是这样的:爱丽丝会给我们看很多她的手印。我们需要分辨出每一个手印究竟是她的左手还是右手。题目里给出了右手掌的形状(由20个顶点构成),而左手掌是右手的镜像对称图形。
我们收到的输入是20个顶点的二维坐标,这些坐标的顺序可能是顺时针的,也可能是逆时针的。而且,整个手印可能会被平移和旋转,但好消息是它不会被放大或缩小。
我们的任务就是写一个程序,对每一个手印,准确地输出 "left" 或 "right"。
解题思路分析
哈喵~ 各位挑战者们,准备好赢得爱丽丝的芳心了吗?这道题看起来有点棘手,手印可以转来转去,坐标变来变去,要怎么认出它呢?
别担心,跟着我的思路,一步步解开这个谜题!
寻找不变的“猫爪印”
首先,题目告诉我们手印可以平移和旋转,但不会缩放。这是一个超级重要的信息,喵!这意味着什么呢?
- 平移和旋转会改变每个点的绝对坐标。
- 但是,图形本身的几何特性是不变的!比如,任意两条相邻边之间的长度和它们之间的夹角都是固定的。
这就好比我的爪印,不管我怎么跑怎么跳,爪印的形状总是一样的呀!所以,我们的策略就是,从这个乱糟糟的手印里,找到一个独一無二的、像“胎记”一样的几何特征。
锁定独特的几何特征
让我们来仔细观察一下题目给出的右手手掌的形状。它是由20条线段连接而成的。我们可以计算一下这些线段的长度。为了方便,我们把题目图片里的点在脑海里建立一个坐标系,就像参考代码3那样,比如让最左下角的点是 (1, 0)。
通过计算(或者偷偷看一下参考代码算出的结果~),我们可以得到所有20条边的长度序列。仔细检查后,我们会发现一个非常特别的地方:
有一个顶点,它连接的两条边的长度分别是 9 和 6。 (在参考代码给出的坐标系中,这是点 (1,0),连接着到 (10,0) 的长度为9的边和到 (1,6) 的长度为6的边。)
这个“连接着长度为9和长度为6的边的顶点”的特征,在整个手掌上是独一无二的!
耶!我们找到了一个完美的“猫爪印”!无论手印怎么旋转和平移,这个特征点(我们叫它“拇指关节”好了)和它那两条特殊长度的邻边总是存在的。我们只要遍历输入的20个点,找到这个点就行啦!
用叉积分辨左右手
找到了这个“拇指关节” V 之后,我们怎么区分左右手呢?
左手是右手的镜像。在几何上,镜像操作会改变一个东西的“手性”(Chirality)。在二维平面里,判断手性的最佳工具就是向量叉积(Cross Product),喵!
假设我们找到了那个特征点 V。它有两个邻居,我们称之为 N1 和 N2。一个邻居在长度为9的边上,另一个在长度为6的边上。我们可以从 V 点出发,定义两个向量:
v_9: 从V指向长度为9的边的另一个端点。v_6: 从V指向长度为6的边的另一个端点。
现在,我们来计算这两个向量的叉积: v_9 × v_6。 叉积 a × b 的几何意义是向量 a 逆时针旋转到向量 b 所扫过的平行四边形的有向面积。
- 如果结果为正,表示
a到b是一个逆时针(左)转。 - 如果结果为负,表示
a到b是一个顺时针(右)转。 - 如果为零,表示它们共线。
对于标准的右手手掌,我们来计算一下这个叉积的符号。假设 V 是 (1,0),v_9 的终点是 (10,0),v_6 的终点是 (1,6)。 那么 v_9 = (10-1, 0-0) = (9, 0)。 v_6 = (1-1, 6-0) = (0, 6)。
结果是正数!
现在,想象一下左手,它是右手的镜像。v_9 可能会变成 (-9, 0)(如果沿y轴镜像),但 v_6 保持 (0, 6)。叉积就变成了 -9 * 6 - 0 * 0 = -54。符号翻转了!
所以,我们的最终策略就是:
- 遍历输入的20个点,找到那个连接着长度约为9和6的边的“拇指关节”点
V。 - 确定从
V出发的两个向量v_9和v_6。 - 计算叉积
v_9 × v_6。 - 如果叉积是正数,说明它是 右手。
- 如果叉积是负数,说明它是 左手。
这个方法非常巧妙,因为它不受顶点输入顺序(顺时针或逆时针)的影响。我们是根据边的长度来确定 v_9 和 v_6,而不是根据输入点的prev和next顺序,所以计算出的叉积对于同一个手来说符号是恒定的。
好啦,思路清晰了,让我们动手实现吧,喵~
代码实现
#include <iostream>
#include <vector>
#include <cmath>
#include <numeric>
// 定义一个点结构体,让代码更清晰喵~
struct Point {
double x, y;
};
// 计算两点之间距离的平方,避免不必要的开方运算,可以减少精度误差
double dist_sq(Point p1, Point p2) {
return (p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y);
}
// 解决单个测试用例的函数
void solve() {
std::vector<Point> points(20);
for (int i = 0; i < 20; ++i) {
std::cin >> points[i].x >> points[i].y;
}
// 浮点数比较需要一个小的容差范围 (epsilon)
const double EPS = 1e-4;
// 目标特征:边长的平方是 9*9=81 和 6*6=36
const double len_sq_9 = 81.0;
const double len_sq_6 = 36.0;
for (int i = 0; i < 20; ++i) {
// 获取当前点和它的两个邻居
Point current_p = points[i];
// 用取模运算处理环形数组的边界情况,喵~
Point prev_p = points[(i + 19) % 20];
Point next_p = points[(i + 1) % 20];
// 计算当前点到两个邻居的距离的平方
double d_sq_prev = dist_sq(current_p, prev_p);
double d_sq_next = dist_sq(current_p, next_p);
// 检查是否是我们要找的特征点
bool is_feature_point = false;
if (std::abs(d_sq_prev - len_sq_9) < EPS && std::abs(d_sq_next - len_sq_6) < EPS) {
is_feature_point = true;
} else if (std::abs(d_sq_prev - len_sq_6) < EPS && std::abs(d_sq_next - len_sq_9) < EPS) {
is_feature_point = true;
}
if (is_feature_point) {
// 找到了!现在定义从当前点出发的两个向量
Point vec_a = {prev_p.x - current_p.x, prev_p.y - current_p.y};
Point vec_b = {next_p.x - current_p.x, next_p.y - current_p.y};
Point vec_len9, vec_len6;
// 根据长度,正确地给 v_9 和 v_6 赋值
// 这样就不用管输入点的顺时针/逆时针顺序了
if (std::abs(d_sq_prev - len_sq_9) < EPS) {
vec_len9 = vec_a;
vec_len6 = vec_b;
} else {
vec_len9 = vec_b;
vec_len6 = vec_a;
}
// 计算 v_9 和 v_6 的叉积
double cross_product = vec_len9.x * vec_len6.y - vec_len9.y * vec_len6.x;
// 根据叉积的符号判断左右手
if (cross_product > 0) {
std::cout << "right\n";
} else {
std::cout << "left\n";
}
// 找到答案了,就可以结束这个测试用例的循环了
return;
}
}
}
int main() {
// 加速输入输出,让程序跑得像猫一样快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
int t;
std::cin >> t;
while (t--) {
solve();
}
return 0;
}复杂度分析
时间复杂度: ,其中 是测试用例的数量。 对于每个测试用例,我们最多遍历一次20个顶点。在循环中,我们进行的是常数时间的计算。因为顶点数 是一个常数,所以处理单个测试用例的复杂度是 ,也就是 。总的时间复杂度就是 。
空间复杂度: 。 对于每个测试用例,我们只需要一个大小为20的
vector来存储顶点坐标。因为这个大小是固定的,所以占用的额外空间是常数级别的,即 。
知识点总结
这道题虽然伪装成了复杂的几何题,但核心思想却非常精妙,喵~
- 寻找不变量: 面对几何体的平移、旋转等变换时,一个强大的解题思想是寻找那些在变换中保持不变的性质(Geometric Invariants),例如边长、角度、面积等。
- 唯一特征定位: 在一个复杂的图形中,可以尝试寻找一个或一组独特的局部特征,作为识别和定位的“锚点”。
- 叉积的应用: 二维向量的叉积是计算几何中的利器!它的符号可以用来判断旋转方向、点与线的相对位置,以及本题中的“手性”(chirality)。
a × b > 0通常表示从a到b是逆时针旋转。 - 浮点数处理: 在计算几何中,直接用
==比较浮点数是危险的。正确的做法是判断两个浮点数之差的绝对值是否小于一个极小的数EPS(epsilon)。
掌握了这些技巧,再遇到类似的几何谜题,相信你也能像聪明的我一样轻松解开啦,加油哦,喵~!