Magic Line - 题解
标签与难度
标签: 几何, 排序, 构造, 分治思想, 计算几何入门 难度: 1400
题目大意喵~
米娜桑,こんにち喵!(ฅ'ω'ฅ)
这道题是说,在一个二维平面上,散落着 N 个不同的点点。我们的任务是画一条“魔法直线”,把这些点点正好分成数量相等的两半,也就是每半边各有 N/2 个点。还有一个重要的要求,就是这条直线不能穿过任何一个点哦!
输入会告诉我们点的数量 N 和每个点的坐标 (x, y)。我们需要输出两个点的坐标,这两个点可以定义出那条满足条件的魔法直线。
举个栗子,如果有4个点,我们就要画一条线,让线的两边各有2个点,而且线不能碰到任何点。很简单吧?喵~
解题思路分析
这道题看起来像是一道几何题,但其实核心思想非常简单,就像猫猫找到最舒服的打盹位置一样,我们只需要找到一个最舒服的“分割位置”就可以啦,喵!
要把点分成“左边”和“右边”两部分,最直观的想法是什么呢?当然是根据它们的 x坐标 来排排坐啦!
第一步:排序
我们可以把所有的 N 个点,都按照 x 坐标从小到大进行排序。如果 x 坐标相同,那就再按照 y 坐标从小到大排序。这样一来,所有的点点就整齐地从左到右(或者从下到上)排列好了。
排序后,我们就有了一个点序列 。因为我们要把它们分成数量相等的两半,所以分割线自然就应该出现在最中间的两个点之间,也就是点 和点 之间。
我们把这两个关键的点叫做 p_left_half_end 和 p_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 and p_right_half_start as .
情况一:
这是最简单的情况!两个关键点的 x 坐标不同,说明在 x 轴上,两组点之间存在一条“垂直的空隙”。所有前半部分的点的 x 坐标都 ,所有后半部分的点的 x 坐标都 。
我们只需要画一条线,穿过 和 之间的区域就可以啦!
一个非常巧妙的构造方法是:取两个点来定义这条线,一个在 的垂线上,另一个在 的垂线上。比如,我们可以取点 和点 。
连接 和 的直线会非常非常陡峭,它的 x 轴截距(也就是与 x 轴的交点)正好是 。这条线完美地从两组点之间的空隙穿过,绝对不会碰到任何点,任务完成,喵~
情况二:
这种情况稍微复杂一点点。当 时,说明最中间的这两个点,以及可能还有其他一些点,都在同一条垂直线上。我们称这个公共的 x 坐标为 。
因为我们排序时,如果 x 相同会按 y 排序,所以我们一定有 。
现在,我们不能用垂直线来分割了,因为它会穿过这些点。我们需要一条几乎水平的线,让它从点 和点 之间的“水平空隙”穿过。
怎么构造呢?我们可以让这条线在 处,其 y 坐标恰好位于 和 的中间,也就是 。
为了用两个整数坐标的点来定义这条线,我们可以这样做: 取点 和点 。
这条线通过点 和 。让我们看看当 时,这条线的 y 坐标是多少。根据中点公式,它正好是 和 的平均值,即 !
因为 ,所以这条线在 处,恰好从 和 之间穿过。而且这条线非常平缓,它几乎是水平的,不会碰到其他任何点,非常安全,喵!
这样,两种情况都被我们完美解决啦!是不是感觉自己也充满了魔力呢?(ฅ^•ﻌ•^ฅ)
代码实现
下面就是我根据上面的思路,精心为您准备的代码实现啦!注释写得很详细,希望能帮助你理解哦~
#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;
}复杂度分析
时间复杂度: 主要的时间开销在于对
N个点进行排序,std::sort的平均时间复杂度是 。剩下的操作都是常数时间 的,所以总的时间复杂度就是排序的复杂度,的说。空间复杂度: 我们需要一个
std::vector来存储N个点的信息,所以空间复杂度是 。没有使用其他需要大量空间的辅助数据结构,喵~
知识点总结
这道题虽然简单,但也是对思维的一个小小锻炼呢!我们来总结一下学到了什么吧:
- 排序思想: 面对一堆无序的点,排序是解决很多几何问题的基本且强大的第一步。通过排序,我们可以将问题从无序变为有序,从而发现规律。
- 分治与构造: 我们通过排序找到了问题的“中点”,然后针对这个中点构造出解。这种“找到关键点,然后构造解”的思路在算法竞赛中非常常见。
- 处理边界情况 (Edge Cases): 我们需要仔细考虑 和 这两种情况。在编程中,清晰地处理各种边界情况是写出健壮代码的关键,就像猫猫总能找到最稳当的着陆点一样,喵~
- 几何构造技巧: 学会了如何用两个整数点来定义一条满足特定条件的直线(比如,穿过某两个点之间的中点,或者穿过某两个 x 坐标之间的区域)。
希望这篇题解能帮助你更好地理解这道题!继续加油哦,你也是最棒的,喵!(>ω<)ノ