扫描线算法

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)、终点 (覆盖数 - 1)
  2. 排事件:按坐标升序;坐标相同时起点事件必须排在终点前(满足闭区间覆盖要求)
  3. 扫事件:维护当前重叠数(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;
}
相关推荐
落羽的落羽1 小时前
【项目】JsonRpc框架——功能测试、项目总结
linux·服务器·开发语言·c++·qt·算法·机器学习
无限码力1 小时前
华为非AI方向笔试真题-昇腾NPU协同调度系统(详细思路+多语言题解)
算法·华为·华为机试·华为笔试真题·华为非ai笔试真题
小蒋学算法1 小时前
算法-掉落的方块-线段树
数据结构·算法
Brilliantwxx1 小时前
【算法从零到千】【8-15】滑动窗口
数据结构·算法
超梦dasgg1 小时前
经典的求解图的所有最大完全子图的算法
算法
Lucis__1 小时前
图的高阶算法:从构造最小生成树到求解最短路径问题
数据结构·c++·算法·图论
cheems95271 小时前
[算法手记] 动态规划: 子数组问题
算法
薇茗1 小时前
【小编的精选算法题库】
算法·精选算法题库