简要题意
平面坐标系中有若干点,求最近的一对点相距距离是多少。
输入
先输入点的个数n。接着输入n行,每行2个数分别表示x坐标、y坐标。
输出
直接输出一个浮点数,表示所给的所有点中最近的两个点之间的距离。保留2位小数。
样例
输入
10
7 9
-8 -1
-3 -1
1 4
-3 9
6 -4
7 5
6 6
-6 10
0 8
输出
1.41
解释
距离最近的点为7和8,距离为
限制
对于70%的数据,2 ≤ n ≤ 2000,每个点坐标的绝对值不超过105;
对于100%的数据,2 ≤ n ≤ 3×105,每个点坐标的绝对值不超过109。
时间:2 sec
空间:512 MB
分析
直观解法就是依次取2个不同的点然后算它们之间的距离,然后比较并保存最小的那个距离,其时间复杂度为O(n2),是无法通过所有测试用例的。
其实这道题可以用分而治之的思想来求。可以列出方程:
方程中为求解区间[L, R]的状态方程,mid为[L, R]的中心轴点。其正确性不言而喻:先算左半边最小的距离、再算右半边最小的距离,最后算横跨左右的最小距离,然后取其最小值即可。对于独立的左右两边,都可以在平凡的情况的基础上逐步求出,现在问题的重心是如何高效地求解横跨左右的这种情况。
上图左边是要求解的点集合,右边可以被认为是求解的某个过程的快照。
其中可以看到,左右两边的最小距离已经得到,且中心轴(即红线)也画出了。我们可以这样认为:假设目前的最小距离是d,那么对于横跨左右的所有点中,有一类点我们是不用考虑的,即其x轴坐标距离中心轴的距离已经是≥d了,所以说我们接下来仅需要考虑以中心轴(红线)向左右各扩d距离的区域内(即图中灰色部分)的所有点即可。
有证明可知,灰色区域内的任一点,与其相距距离不超过d的点必然是常数量级的(关于这个理论我还没有悟透,就现假设它是成立的)。因此,对于求解灰色区域的点的最小距离,我们可以先将所有点按y轴排序,然后从第一个点开始找起,当找到一个点与其距离超过d,即可停止。这听起来像二重循环,但是有前面的理论支撑,我们可以至多内循环其实是常数量级。
总结一下,该算法总共要做以下几点:
- 根据现有区间[L, R],算x的轴点mid;
- 二分地去分别找左右两边的最小距离,记为d;
- 考虑以mid为中心轴,2d范围内的点,即x∈(mid-d, mid+d),其余点丢弃;
- 将所有点按y轴排序(此步在算法中可与上一步一起计算,仅需O(n)事件);
- 接着对范围内的所有点求相互之间的距离,并保存最小距离。
程序实现
#include <bits/stdc++.h>
using namespace std;
// ================= 代码实现开始 =================
typedef double lf;
typedef long long ll;
const int N = 300005;
// 用于存储一个二维平面上的点
struct ip {
int x, y;
// 构造函数
ip(int x = 0, int y = 0) : x(x), y(y) { }
// 先比较x轴,再比较y轴
bool operator < (const ip &a) const {
return x == a.x ? y < a.y : x < a.x;
}
} a[N], b[N];
// 计算x的平方
ll sqr(const ll &x) {
return x * x;
}
// 计算点a和点b的距离
lf dis(const ip &a, const ip &b) {
return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y));
}
void swap(ip& p1, ip& p2){
ip p3 = p1;
p1 = p2;
p2 = p3;
}
double ans = 0.0;
// 区间:[L, R]
void solve(int L, int R){
// 递归基
if(R - L <= 1){
if(a[R].y < a[L].y)
swap(a[L], a[R]);
if(L != R)
ans = min(ans, dis(a[L], a[R]));
return;
}
// 分而治之
int mid = (L + R) >> 1;
int md = a[mid].x; //md即范围的中心轴
solve(L, mid);
solve(mid+1, R);
int cnt = 0;
// 处理以md为中心轴,两侧距离不超过d的所有点,d即此时的ans
for(int i=L, j=mid+1; i <= mid || j <= R; ){
for( ; i<=mid && ans <= md-a[i].x; ++i);//去除左边所有不在d范围内的点
for( ; j<=R && ans <= a[j].x-md; ++j); //去除右边所有不在d范围内的点
if(i <= mid && (R < j || a[i].y < a[j].y))//此处按y轴非降序排序
b[cnt++] = a[i++];
else
b[cnt++] = a[j++];
}
for(int i=0; i < cnt; ++i){
for(int j=i+1; j < cnt && b[j].y-b[i].y < ans; ++j)//仅考虑与点i的y轴距离小于d的点,可证明其为常数量级;d即此时的ans
ans = min(ans, dis(b[i], b[j]));
}
cnt = 0;
for(int i=L, j=mid+1; i <= mid || j <= R; ){
if(i <= mid && (R < j || a[i].y < a[j].y))//此处按y轴非降序排序
b[cnt++] = a[i++];
else
b[cnt++] = a[j++];
}
memcpy(a+L, b, sizeof(ip)*cnt);
}
/* 请在这里定义你需要的全局变量 */
// 计算最近点对的距离
// n:n个点
// X, Y:分别表示x轴坐标和y轴坐标,下标从0开始
// 返回值:最近的距离
double getAnswer(int n, vector<int> X, vector<int> Y) {
/* 请在这里设计你的算法 */
for(int i=0; i<n; ++i)
a[i+1] = ip(X[i], Y[i]);
ans = INT_MAX;
sort(a+1, a+n+1);//先排序:将所有点按现x后y的方式进行排序
solve(1, n);
return ans;
}
// ================= 代码实现结束 =================
int main() {
int n;
scanf("%d", &n);
vector<int> X, Y;
for (int i = 1; i <= n; ++i) {
int x, y;
scanf("%d%d", &x, &y);
X.push_back(x);
Y.push_back(y);
}
printf("%.2f\n", getAnswer(n, X, Y));
return 0;
}
来源
邓俊辉老师的《算法训练营》第一期。