Skip to content

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。它有两个邻居,我们称之为 N1N2。一个邻居在长度为9的边上,另一个在长度为6的边上。我们可以从 V 点出发,定义两个向量:

  • v_9: 从 V 指向长度为9的边的另一个端点。
  • v_6: 从 V 指向长度为6的边的另一个端点。

现在,我们来计算这两个向量的叉积: v_9 × v_6。 叉积 a × b 的几何意义是向量 a 逆时针旋转到向量 b 所扫过的平行四边形的有向面积。

  • 如果结果为正,表示 ab 是一个逆时针(左)转。
  • 如果结果为负,表示 ab 是一个顺时针(右)转。
  • 如果为零,表示它们共线。

对于标准的右手手掌,我们来计算一下这个叉积的符号。假设 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)

cross_product=v9.xv6.yv9.yv6.x=9×60×0=54\text{cross\_product} = v_{9}.x \cdot v_{6}.y - v_{9}.y \cdot v_{6}.x = 9 \times 6 - 0 \times 0 = 54

结果是正数!

现在,想象一下左手,它是右手的镜像。v_9 可能会变成 (-9, 0)(如果沿y轴镜像),但 v_6 保持 (0, 6)。叉积就变成了 -9 * 6 - 0 * 0 = -54。符号翻转了!

所以,我们的最终策略就是:

  1. 遍历输入的20个点,找到那个连接着长度约为9和6的边的“拇指关节”点 V
  2. 确定从 V 出发的两个向量 v_9v_6
  3. 计算叉积 v_9 × v_6
  4. 如果叉积是正数,说明它是 右手
  5. 如果叉积是负数,说明它是 左手

这个方法非常巧妙,因为它不受顶点输入顺序(顺时针或逆时针)的影响。我们是根据边的长度来确定 v_9v_6,而不是根据输入点的prevnext顺序,所以计算出的叉积对于同一个手来说符号是恒定的。

好啦,思路清晰了,让我们动手实现吧,喵~

代码实现

cpp
#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;
}

复杂度分析

  • 时间复杂度: O(T)O(T),其中 TT 是测试用例的数量。 对于每个测试用例,我们最多遍历一次20个顶点。在循环中,我们进行的是常数时间的计算。因为顶点数 N=20N=20 是一个常数,所以处理单个测试用例的复杂度是 O(N)=O(20)O(N) = O(20),也就是 O(1)O(1)。总的时间复杂度就是 O(T×1)=O(T)O(T \times 1) = O(T)

  • 空间复杂度: O(1)O(1)。 对于每个测试用例,我们只需要一个大小为20的vector来存储顶点坐标。因为这个大小是固定的,所以占用的额外空间是常数级别的,即 O(1)O(1)

知识点总结

这道题虽然伪装成了复杂的几何题,但核心思想却非常精妙,喵~

  1. 寻找不变量: 面对几何体的平移、旋转等变换时,一个强大的解题思想是寻找那些在变换中保持不变的性质(Geometric Invariants),例如边长、角度、面积等。
  2. 唯一特征定位: 在一个复杂的图形中,可以尝试寻找一个或一组独特的局部特征,作为识别和定位的“锚点”。
  3. 叉积的应用: 二维向量的叉积是计算几何中的利器!它的符号可以用来判断旋转方向、点与线的相对位置,以及本题中的“手性”(chirality)。a × b > 0 通常表示从ab是逆时针旋转。
  4. 浮点数处理: 在计算几何中,直接用 == 比较浮点数是危险的。正确的做法是判断两个浮点数之差的绝对值是否小于一个极小的数 EPS (epsilon)。

掌握了这些技巧,再遇到类似的几何谜题,相信你也能像聪明的我一样轻松解开啦,加油哦,喵~!