套圈
Description
Have you ever played quoit in a playground? Quoit is a game in which flat rings are pitched at some toys, with all the toys encircled awarded. In the field of Cyberground, the position of each toy is fixed, and the ring is carefully designed so it can only encircle one toy at a time. On the other hand, to make the game look more attractive, the ring is designed to have the largest radius. Given a configuration of the field, you are supposed to find the radius of such a ring. Assume that all the toys are points on a plane. A point is encircled by the ring if the distance between the point and the center of the ring is strictly less than the radius of the ring. If two toys are placed at the same point, the radius of the ring is considered to be 0.
Input
The input consists of several test cases. For each case, the first line contains an integer N (2 <= N <= 100,000), the total number of toys in the field. Then N lines follow, each contains a pair of (x, y) which are the coordinates of a toy. The input is terminated by N = 0.
Output
For each test case, print in one line the radius of the ring required by the Cyberground manager, accurate up to 2 decimal places.
Sample Input
2
0 0
1 1
2
1 1
1 1
3
-1.5 0
0 0
0 1.5
0
Sample Output
0.71
0.00
0.75
题目描述
给定一些玩具的位置,求出距离最近的一对玩具之间的距离的一半(即最近点对问题)。需要注意的是,求出最近距离之后还要除以2,因为所要求的其实是套圈所使用的圈的半径。
问题分析
数据范围是2 <= N <= 100,000,所以如果枚举每两个玩具之间的距离,时间复杂度为O(n2),肯定是不可行的。我们可以采用分治法解决这一问题。课本《计算机算法设计与分析(第5版)》(王晓东)中给出了分治法计算最近点对问题的思路,大致如下:
- 设m为N个点的x坐标的中位数;
- 将所有点划分为两个集合:S1——x坐标小于等于m的点集;S2——x坐标大于m的点集;
- 递归地求解S1与S2中的最近点对距离,分别设为d1和d2;
- 设dm=min(d1,d2),设P1是S1中x坐标与m之差的绝对值小于dm的点集,P2是S2中x坐标与m之差的绝对值小于dm的点集;
- 将P1与P2中的点按照y坐标排序,设X和Y分别是排好序的点列;
- 对于X中的每一个点,检查Y中与其距离在dm之内的所有点(由鸽巢原理可以证明最多有6个点,详见课本),即Y中的扫描指针可以在宽度为2dm的一个区间内移动,求出最小距离为d1;
- 所有点的最近点对距离即为min(dm,d1)。
和上一篇博客一样,这里要吐槽一下课本。课本上的代码先是给出了两个点类(class),还有运算符重载,最后计算最近点对距离的函数有8个参数,还不告诉都是什么含义,代码中间的注释也极少,导致在学习时看那段代码就看了得有一两个小时才明白在干什么……并且,上述思路的第6步,看起来很合理,节约了时间,但实际上这不是一种计算思维的描述,实现起来也不好实现,并且我感觉课本上给出的代码,也与这个思路不太一样,也是把所有P1与P2中的点对都考察了一遍,感觉并不能减少for循环的执行次数……
下面的AC代码将上面思路的第4-7步修改如下,同样能够通过本题:
- 设dm=min(d1,d2),设P为S1和S2中x坐标与m之差的绝对值小于dm的点集;
- 将P中的点按照y坐标排序;
- 对于P中的任意两点,如果它们的y坐标之差小于dm,则计算它们之间的距离,如果小于dm,则更新dm;
- 最后dm的值即为最小距离。
AC代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
struct point
{
double x,y;
}p[100005];
int q[100005];
bool cmpx(point p,point q) //x坐标排序比较函数
{
return p.x<q.x;
}
bool cmpy(int p1,int q) //y坐标排序比较函数
{
return p[p1].y<p[q].y;
}
double dist(point p,point q) //计算距离函数
{
return sqrt((p.x-q.x)*(p.x-q.x)+(p.y-q.y)*(p.y-q.y));
}
double mindist(int l,int r) //计算最近点对距离函数
{
if (r-l==1) return dist(p[l],p[r]); //两个点的情况,直接计算
else if (r-l==2) //三个点的情况
{
double x=dist(p[l],p[l+1]),y=dist(p[l+1],p[r]),z=dist(p[l],p[r]);
if (x<y) swap(x,y);
if (y<z) swap(y,z);
if (x<z) swap(x,z);
return z;
}
else //四个及以上点的情况,分治
{
int mid=(l+r)/2; //中位数
double minn=min(mindist(l,mid),mindist(mid+1,r)); //递归地求两部分的最近点对距离
int k=0;
for (int i=l;i<=r;i++) //找出与中位数x坐标之差绝对值在minn内的点
{
if (fabs(p[i].x-p[mid].x)<minn)
{
q[k]=i; //q存的是点的编号
k++;
}
}
sort(q,q+k,cmpy); //按y坐标排序
for (int i=0;i<k;i++)
{
for (int j=i+1;j<k&&p[q[j]].y-p[q[i]].y<minn;j++) //如果两个点的y坐标之差小于minn,就考察它们之间的距离,如果距离小于minn就更新minn
{
double d=dist(p[q[i]],p[q[j]]);
if (d<minn) minn=d;
}
}
return minn;
}
}
int main()
{
int n;
while (scanf ("%d",&n)!=EOF)
{
if (n==0) break;
for (int i=0;i<n;i++)
{
scanf ("%lf%lf",&p[i].x,&p[i].y);
}
sort(p,p+n,cmpx);
double ans=mindist(0,n-1);
printf ("%.2lf\n",ans/2); //这里别忘了除以2
}
return 0;
}
当时写这个代码的时候,有一点疑问一直不知道是为什么,就是代码的第43行,我存的是点的编号。如果直接存y坐标的话,在杭电OJ和乐学上面提交都会TLE,但存点的编号,然后将按y坐标排序的比较函数那里再去寻找y坐标,第50行的for循环条件改成p[q[i]].y的形式去找y坐标,就不会超时。匪夷所思,最后也没想明白。
而且,我比较怀疑第50行加上p[q[j]].y-p[q[i]].y<minn这个条件之后,究竟能节省多少时间,因为这不能减少for循环的循环次数,节省的只是调用函数求距离的时间。
并且,突然发现了代码写得不太好的地方——我考察的是x坐标与中位数之差的绝对值小于minn的所有点对,其实有相当多一部分是不需要考察的——如果它们同出自点集S1,或同出自点集S2,则它们就不需要考察了。我们只需要考察出自不同点集的两点之间的距离。如果像我这么写的话,貌似按照y坐标排序这一步也可以省去。。。