Skip to content

Magic Line - 题解

标签与难度

标签: 几何, 排序, 构造, 分治思想, 计算几何入门 难度: 1400

题目大意喵~

米娜桑,こんにち喵!(ฅ'ω'ฅ)

这道题是说,在一个二维平面上,散落着 N 个不同的点点。我们的任务是画一条“魔法直线”,把这些点点正好分成数量相等的两半,也就是每半边各有 N/2 个点。还有一个重要的要求,就是这条直线不能穿过任何一个点哦!

输入会告诉我们点的数量 N 和每个点的坐标 (x, y)。我们需要输出两个点的坐标,这两个点可以定义出那条满足条件的魔法直线。

举个栗子,如果有4个点,我们就要画一条线,让线的两边各有2个点,而且线不能碰到任何点。很简单吧?喵~

解题思路分析

这道题看起来像是一道几何题,但其实核心思想非常简单,就像猫猫找到最舒服的打盹位置一样,我们只需要找到一个最舒服的“分割位置”就可以啦,喵!

要把点分成“左边”和“右边”两部分,最直观的想法是什么呢?当然是根据它们的 x坐标 来排排坐啦!

第一步:排序

我们可以把所有的 N 个点,都按照 x 坐标从小到大进行排序。如果 x 坐标相同,那就再按照 y 坐标从小到大排序。这样一来,所有的点点就整齐地从左到右(或者从下到上)排列好了。

排序后,我们就有了一个点序列 P0,P1,,PN1P_0, P_1, \dots, P_{N-1}。因为我们要把它们分成数量相等的两半,所以分割线自然就应该出现在最中间的两个点之间,也就是点 PN/21P_{N/2-1} 和点 PN/2P_{N/2} 之间。

我们把这两个关键的点叫做 p_left_half_endp_right_half_start 吧,听起来就很专业,的说!

p_left_half_end 是前半部分点中,最“右”的一个。 p_right_half_start 是后半部分点中,最“左”的一个。

现在问题就变成了:如何画一条线,把 p_left_half_end 和它左边的所有点,与 p_right_half_start 和它右边的所有点分开呢?

这里就需要一点点分类讨论的智慧啦,就像猫猫决定今天是在窗边晒太阳还是在沙发上打滚一样,需要根据情况选择最佳策略,喵~

Let's denote p_left_half_end as Pa=(xa,ya)P_a = (x_a, y_a) and p_right_half_start as Pb=(xb,yb)P_b = (x_b, y_b).

情况一:xa<xbx_a < x_b

这是最简单的情况!两个关键点的 x 坐标不同,说明在 x 轴上,两组点之间存在一条“垂直的空隙”。所有前半部分的点的 x 坐标都 xa\le x_a,所有后半部分的点的 x 坐标都 xb\ge x_b

我们只需要画一条线,穿过 xax_axbx_b 之间的区域就可以啦!

一个非常巧妙的构造方法是:取两个点来定义这条线,一个在 x=xax=x_a 的垂线上,另一个在 x=xbx=x_b 的垂线上。比如,我们可以取点 L1=(xa,一个很大的正数)L_1 = (x_a, \text{一个很大的正数}) 和点 L2=(xb,一个很大的负数)L_2 = (x_b, \text{一个很大的负数})

L1=(xa,109)L2=(xb,109)L_1 = (x_a, 10^9) \\ L_2 = (x_b, -10^9)

连接 L1L_1L2L_2 的直线会非常非常陡峭,它的 x 轴截距(也就是与 x 轴的交点)正好是 (xa+xb)/2(x_a + x_b) / 2。这条线完美地从两组点之间的空隙穿过,绝对不会碰到任何点,任务完成,喵~

情况二:xa=xbx_a = x_b

这种情况稍微复杂一点点。当 xa=xbx_a = x_b 时,说明最中间的这两个点,以及可能还有其他一些点,都在同一条垂直线上。我们称这个公共的 x 坐标为 xcx_c

因为我们排序时,如果 x 相同会按 y 排序,所以我们一定有 ya<yby_a < y_b

现在,我们不能用垂直线来分割了,因为它会穿过这些点。我们需要一条几乎水平的线,让它从点 Pa=(xc,ya)P_a = (x_c, y_a) 和点 Pb=(xc,yb)P_b = (x_c, y_b) 之间的“水平空隙”穿过。

怎么构造呢?我们可以让这条线在 x=xcx=x_c 处,其 y 坐标恰好位于 yay_ayby_b 的中间,也就是 (ya+yb)/2(y_a + y_b) / 2

为了用两个整数坐标的点来定义这条线,我们可以这样做: 取点 L1=(xc1,yb)L_1 = (x_c - 1, y_b) 和点 L2=(xc+1,ya)L_2 = (x_c + 1, y_a)

这条线通过点 (xc1,yb)(x_c-1, y_b)(xc+1,ya)(x_c+1, y_a)。让我们看看当 x=xcx = x_c 时,这条线的 y 坐标是多少。根据中点公式,它正好是 yby_byay_a 的平均值,即 (ya+yb)/2(y_a + y_b) / 2

因为 ya<(ya+yb)/2<yby_a < (y_a + y_b) / 2 < y_b,所以这条线在 x=xcx=x_c 处,恰好从 PaP_aPbP_b 之间穿过。而且这条线非常平缓,它几乎是水平的,不会碰到其他任何点,非常安全,喵!

这样,两种情况都被我们完美解决啦!是不是感觉自己也充满了魔力呢?(ฅ^•ﻌ•^ฅ)

代码实现

下面就是我根据上面的思路,精心为您准备的代码实现啦!注释写得很详细,希望能帮助你理解哦~

cpp
#include <iostream>
#include <vector>
#include <algorithm>

// 为了清晰,我们定义一个点的结构体,喵~
struct Point {
    long long x, y;
};

// 排序时需要一个比较函数
// 先按 x 坐标排,如果 x 相同,再按 y 坐标排
bool comparePoints(const Point& a, const Point& b) {
    if (a.x != b.x) {
        return a.x < b.x;
    }
    return a.y < b.y;
}

void solve() {
    int n;
    std::cin >> n;
    std::vector<Point> points(n);
    for (int i = 0; i < n; ++i) {
        std::cin >> points[i].x >> points[i].y;
    }

    // 第一步:对所有点进行排序
    std::sort(points.begin(), points.end(), comparePoints);

    // 找到分割两半的关键点
    // 因为是0-based索引,所以是 n/2 - 1 和 n/2
    Point p_left_half_end = points[n / 2 - 1];
    Point p_right_half_start = points[n / 2];

    // 情况一:中间两个点的 x 坐标不同
    if (p_left_half_end.x < p_right_half_start.x) {
        // 构造一条非常陡峭的线,它在 x 轴的截距在两者之间
        // 我们可以取一个很大的Y值来确保它足够陡峭,不会碰到其他点
        long long large_y = 2000000000;
        // 输出定义这条线的两个点
        std::cout << p_left_half_end.x << " " << large_y << " "
                  << p_right_half_start.x << " " << -large_y << std::endl;
    } 
    // 情况二:中间两个点的 x 坐标相同
    else {
        // 此时 p_left_half_end.x == p_right_half_start.x
        // 并且因为排序规则,一定有 p_left_half_end.y < p_right_half_start.y
        // 我们构造一条几乎水平的线,让它从两个点中间穿过
        long long common_x = p_left_half_end.x;
        // 这条线会经过 (common_x, (p_left_half_end.y + p_right_half_start.y)/2)
        // 这是一个非常安全和巧妙的构造方法,喵~
        std::cout << common_x - 1 << " " << p_right_half_start.y << " "
                  << common_x + 1 << " " << p_left_half_end.y << std::endl;
    }
}

int main() {
    // 加速输入输出,让程序跑得像猫一样快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(NlogN)O(N \log N) 主要的时间开销在于对 N 个点进行排序,std::sort 的平均时间复杂度是 O(NlogN)O(N \log N)。剩下的操作都是常数时间 O(1)O(1) 的,所以总的时间复杂度就是排序的复杂度,的说。

  • 空间复杂度: O(N)O(N) 我们需要一个 std::vector 来存储 N 个点的信息,所以空间复杂度是 O(N)O(N)。没有使用其他需要大量空间的辅助数据结构,喵~

知识点总结

这道题虽然简单,但也是对思维的一个小小锻炼呢!我们来总结一下学到了什么吧:

  1. 排序思想: 面对一堆无序的点,排序是解决很多几何问题的基本且强大的第一步。通过排序,我们可以将问题从无序变为有序,从而发现规律。
  2. 分治与构造: 我们通过排序找到了问题的“中点”,然后针对这个中点构造出解。这种“找到关键点,然后构造解”的思路在算法竞赛中非常常见。
  3. 处理边界情况 (Edge Cases): 我们需要仔细考虑 xa<xbx_a < x_bxa=xbx_a = x_b 这两种情况。在编程中,清晰地处理各种边界情况是写出健壮代码的关键,就像猫猫总能找到最稳当的着陆点一样,喵~
  4. 几何构造技巧: 学会了如何用两个整数点来定义一条满足特定条件的直线(比如,穿过某两个点之间的中点,或者穿过某两个 x 坐标之间的区域)。

希望这篇题解能帮助你更好地理解这道题!继续加油哦,你也是最棒的,喵!(>ω<)ノ