395.圈点
题目描述
现在需要在坐标平面上以某一点 C 为圆心画一个圆,且该圆心必须位于坐标轴上。 请你找到一个最小的半径r,使得这圆能够覆盖不少于n / 2个给定点,并输出这个最小半径。
【名词解释】
坐标轴: 包含x轴和y轴。x 轴表示形如(x, 0)的所有点;y轴表示形如(0, y)的所有点。 覆盖:若点 P 到圆心 C 的欧氏距离不超过圆的半径,则称该圆覆盖点 P。
输入描述
第一行输入一个整数n (2 <= n <= 105),表示点的数量。 接下来n行,每行输入两个整数 xi, yi(-105 <= xi, yi <= 105),表示第i个点的坐标。
输出描述
输出一个实数,表示能够覆盖至少n / 2个给定点的、以坐标轴上某点为圆心的最小圆的半径。 保留6位小数。
- 输入示例
4
-1 0
1 0
0 1
100 100
- 输出示例
1.000000
提示信息
时间限制:c/c++:1s;java:8s;其他语言:5s。
思路:扫描线算法 + 二分查找
1. 问题转化
以圆心(X, 0)在x轴上为例:
(X−xi)2+yi2≤r2 (X - x_i)^2 + y_i^2 \le r^2 (X−xi)2+yi2≤r2
-
显然,当 ∣yi∣>r|y_i| \gt r∣yi∣>r 时,一定不成立(剪枝)
-
当 ∣yi∣≤r|y_i| \le r∣yi∣≤r 时,需要满足:
xi−r2−yi2≤X≤xi+r2−yi2 x_i - \sqrt{r^2 - y_i^2} \le X \le x_i +\sqrt{r^2 - y_i^2} xi−r2−yi2 ≤X≤xi+r2−yi2
于是,问题就变成了:
给定n个区间的起点 (xi−r2−yi2x_i - \sqrt{r^2 - y_i^2}xi−r2−yi2 ) 和终点 (xi+r2−yi2x_i + \sqrt{r^2 - y_i^2}xi+r2−yi2 ) ,找出最小的r,使得至少有一半的区间重叠。
2. 扫描线算法
通用三步法:
- 拆事件:每个区间拆为两个事件:起点 (覆盖数 + 1)、终点 (覆盖数 - 1)
- 排事件:按坐标升序;坐标相同时起点事件必须排在终点前(满足闭区间覆盖要求)
- 扫事件:维护当前重叠数(
current_overlap),遍历过程中更新答案
关键实现细节:
- 事件编码技巧:起点用-1,终点用1,利用pair默认排序自动满足同坐标起点在前
- 优化:若只需判断是否≥k,重叠数达到 k 时可直接返回,无需遍历全部事件
3. 半径r越大,覆盖的点数单调不减 --> 二分查找
−1e5≤xi,yi≤1e5-1e5 \le x_i, y_i \le 1e5−1e5≤xi,yi≤1e5
rrr 的最大可能为
rmax=(2e5)2+(2e5)2≈8e5<3e5 r_{max} = \sqrt{(2e5)^2 + (2e5)^2} \approx \sqrt{8}e5 \lt 3e5 rmax=(2e5)2+(2e5)2 ≈8 e5<3e5
二分查找的范围为:0,3e50, 3e50,3e5 ,迭代约 65 次即可达到 10−610^{-6}10−6 以下的精度。
3e5/265<10−7 3e5 / 2^{65} \lt 10^{-7} 3e5/265<10−7
4. 复杂度分析:
- 时间复杂度:O(I * n * log n)
其中 I 为二分查找的迭代次数(约为 65 次),每次检查需要对最多 2n 个事件进行排序,排序的时间复杂度为 O(n log n),遍历事件的时间复杂度为 O(n)。整体时间在接受范围内。 - 空间复杂度:O(n)
用于在每次验证函数中存储所有的扫描线区间事件
5. 代码实现:
C++
cpp
#include <iostream>
#include <vector>
#include <algorithm>
#include <iomanip> //控制输出精度
#include <cmath>
using namespace std;
//点:double类型
struct Point {
double x, y;
Point(double a, double b)
: x(a), y(b)
{}
};
int n, k;
vector<Point> points;
//扫描线算法:判断是x轴/y轴上的圆的半径是否符合条件
bool check_axis(double r, bool is_x) {
int count = 0;//可能覆盖到的点的数量
vector<pair<double, int>> events;
events.reserve(n * 2);
for (const auto& p : points) {
double dist = is_x ? abs(p.y) : abs(p.x);
double proj = is_x ? p.x : p.y;
if (dist <= r) {
++count;
double d = sqrt(r * r - dist * dist);
events.emplace_back(proj - d, -1);//起点事件
events.emplace_back(proj + d, 1);//终点事件
}
}
//如果有可能被覆盖的点的数量少于k,直接返回false
if (count < k) {
return false;
}
//将所有事件从小到大排序,相同保证起点事件排在前面
sort(events.begin(), events.end());
int current_overlap = 0;//重叠区间的数量
for (const auto& e : events) {
current_overlap -= e.second;
if (current_overlap >= k) {
return true;
}
}
return false;
}
//半径r是否符合要求
bool check(double r) {
return check_axis(r, true) || check_axis(r, false);
}
int main() {
//优化输入输出
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
k = (n + 1) / 2;//向上取整
points.reserve(n * 2);
for (int i = 0; i < n; ++i) {
double x, y;
cin >> x >> y;
points.emplace_back(x, y);
}
//二分查找最小的符合条件的半径
double low = 0, high = 3e5;// -1e5 ~ 1e5
double best_r = high;
for (int i = 0; i < 65; ++i) {//迭代65次,保证精度
double mid = low + (high - low) / 2;
if (check(mid)) {
best_r = mid;
high = mid;//查找更小的答案
} else {
low = mid;//小的不满足,从更大的里面找
}
}
cout << fixed << setprecision(6) << best_r << '\n';
return 0;
}