一、题目解读
1、原题
2、分类
分治法——最近点对
3、题意
给定一些点,求一个圆的半径、满足“圆最多只能使1个点在其内部”。
再稍微转化一下,就是求一堆点里最小的两点间距,然后再除以 2 2 2。
4、输入输出格式
输入/输出 | 要求与格式 |
---|---|
输入样例个数 | 通过输入 N = 0 N=0 N=0标识输入结束 |
输入格式(每个样例) | 第一行输入一个数 N N N,后 N N N行每行都输入一组坐标 x x x、 y y y(空格隔开) |
输出格式(每个样例) | 每行输出一个结果 |
输出精度 | 结果精确到小数点后 2 2 2位 |
5、数据范围
数据 | 范围 |
---|---|
N N N | 2 ≤ N ≤ 1 0 5 2 \leq N \leq 10^5 2≤N≤105 |
( x , y ) (x, y) (x,y) | x , y ∈ R x, y \in \mathbb{R} x,y∈R |
二、题解参考
1、总体思路
思路 | 时间复杂度 | 具体解释 |
---|---|---|
穷举法 | O ( n 2 ) O(n^2) O(n2) | 穷举所有两个点间的距离,找最小 |
分治法 | O ( n log n ) O(n\log n) O(nlogn) | 求左右两个半区间的最小值,然后考虑跨区间的合并 |
2、思路②
(1).分析
分治法的主要思想是将大的问题划分为若干个小的子问题。
在这里主要就表现为,求若干个点的最小距离困难,但是求2、3个点的最小距离容易。因此,对n个点,我们先根据点的横坐标 x x x进行排序,不断地划分左、右区间,一直划分到只剩2、3个点。
那么很显然,得到的是两个子区间各自内部的距离最小值,我们对这两个值求一个最小值得到 d d d.
跨区间的部分要怎么考虑合并呢?跨区间的部分的合并显然不能一个一个列举,还是会变成穷举的时间复杂度 O ( n 2 ) O(n^2) O(n2).因此,需要考虑优化。
仔细想想的话,我们可以肯定横坐标范围在 [ x m i d − d , x m i d + d ] \left[ x_{mid} - d, x_{mid} + d \right] [xmid−d,xmid+d]之外的点都不用考虑。因为要跨越左右区间,所以横坐标 x m i d x_{mid} xmid一定会在被比较的两个点的横坐标之间。
以 x l e f t _ i < x m i d − d x_{left\_i} < x_{mid} - d xleft_i<xmid−d为例:
∵ x l e f t _ i < x m i d − d 且 x r i g h t _ j > x m i d ∴ x r i g h t _ j − x l e f t _ i > d 再 结 合 两 点 间 坐 标 公 式 , 易 知 : l e f t _ i 和 r i g h t _ j 这 两 个 点 间 距 必 定 大 于 d \begin{aligned} &\because x_{left\_i} < x_{mid} - d且x_{right\_j} > x_{mid} \\ &\therefore x_{right\_j} - x_{left\_i} > d \\ &再结合两点间坐标公式,易知:\\ &left\_i和right\_j这两个点间距必定大于d \end{aligned} ∵xleft_i<xmid−d且xright_j>xmid∴xright_j−xleft_i>d再结合两点间坐标公式,易知:left_i和right_j这两个点间距必定大于d
但是这样筛选出来的点仍然有可能有很多个,直接逐个比较的话仍然会超时,因此我们还需要根据其纵坐标 y y y再进行一次优化。
我们将选出来的
c
n
t
cnt
cnt个点根据纵坐标再进行一次升序排序,然后从前往后逐个求距离:第
1
1
1个点逐个和后面
c
n
t
−
1
cnt - 1
cnt−1个点求距离更新
d
d
d、第
2
2
2个点逐个和后面
c
n
t
−
2
cnt - 2
cnt−2个点求距离更新
d
d
d、……但是在比较的时候,如果第
j
j
j个点的纵坐标
y
j
y_j
yj已经比第
i
i
i个点的纵坐标
y
i
y_i
yi多出
d
d
d,即
y
j
−
y
i
>
d
y_j - y_i > d
yj−yi>d,那么从第
j
j
j个点开始往后的点都不可能起到更新
d
d
d的作用,所以直接break
出去,开始外层循环的下一次循环。
这样优化以后,效率会好很多。(印象中当时老师讲的时候说过,有人证明了XXXX,说明了XXXXX最多只会有6个点,所以效率会好很多)
(注):本文的代码参考了这篇文章的代码,因此代码相似度将近95%,本文的思路是对这个代码进行分析理解得到的。
(2).AC代码
HDU(C++/G++)AC代码如下:
#include <iostream>
#include <algorithm>
#include <cmath>
#include <iomanip>
#define N 100005
using namespace std;
struct node
{
double x;
double y;
}us[N];
int a[N];
// 将坐标根据x升序排列
bool cmp_x(const node& a, const node&b)
{
return a.x < b.x;
}
// 将坐标索引根据对应的y升序排列进行排列
bool cmp_yi(int a, int b)
{
return us[a].y < us[b].y;
}
// 计算两点间距
inline double dis(node p1, node p2)
{
return sqrt((p1.x - p2.x) * (p1.x - p2.x) + (p1.y - p2.y) * (p1.y - p2.y));
}
double find(int l, int r)
{
// 分治到只剩两个点
if (l + 1 == r)
return dis(us[l], us[r]);
// 分治到只剩三个点
if (l + 2 == r)
return min(min(dis(us[l], us[l + 1]), dis(us[l + 1], us[r])), dis(us[l], us[r]));
// 寻找左、右半个区间内的最大、最小距离
int mid = (l + r) >> 1;
double d = min(find(l, mid), find(mid + 1, r));
// 合并左右区间的最小距离
// (在x ∈ [mid.x - d, mid.x + d]的范围内寻找,再根据纵坐标排序,效率可以提高很多)
int cnt = 0;
for (int i = l; i <= r; ++i)
if (us[i].x >= us[mid].x - d && us[i].x <= us[mid].x + d)
a[cnt++] = i;
sort(a, a + cnt, cmp_yi);
for (int i = 0; i < cnt; ++i)
for (int j = i + 1; j < cnt; ++j)
{
if (us[a[j]].y - us[a[i]].y >= d)
break;
d = min(d, dis(us[a[i]], us[a[j]]));
}
return d;
}
int main()
{
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int n;
while (cin >> n, n)
{
// 输入
for (int i = 0; i < n; ++i)
cin >> us[i].x >> us[i].y;
// 以x升序、将n个坐标进行排序
sort(us, us + n, cmp_x);
// 输出分治法查找的结果
cout << fixed << setprecision(2) << find(0, n - 1) / 2 << endl;
}
return 0;
}
三、总结与后话
1、评价
这道题目是一道很典型的分治法例题——“求最近点对”。
2、后话
看了10min题目,才想起来这是去年暑假培训的时候老师讲过的;搜了又搜,才想起来这道题题型是“求最近点对”。
当时将分治法、分治思想的时候,感觉还是有所领悟的(除了二分答案我有点懵),半年内,刷的题合起来总共不到30题。到了半年后的今天,居然连分治法的结构长什么样子都记不太清楚了,实在是丢人。
也当做是个毒鸡汤,警示所有人,ACM没有持续和稳定的刷题练习是很难有所长进的。