最近点对:分而治之

简要题意

平面坐标系中有若干点,求最近的一对点相距距离是多少。

输入

先输入点的个数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,距离为 ( 7 6 ) 2 + ( 5 6 ) 2 = 2 1.41 \sqrt[]{(7-6)^2+(5-6)^2} = \sqrt[]{2}≈1.41

限制

对于70%的数据,2 ≤ n ≤ 2000,每个点坐标的绝对值不超过105
对于100%的数据,2 ≤ n ≤ 3×105,每个点坐标的绝对值不超过109
时间:2 sec
空间:512 MB

分析

  直观解法就是依次取2个不同的点然后算它们之间的距离,然后比较并保存最小的那个距离,其时间复杂度为O(n2),是无法通过所有测试用例的。
  其实这道题可以用分而治之的思想来求。可以列出方程:
s o l v e ( L , R ) = m i n { s o l v e ( L , m i d ) , s o l v e ( m i d + 1 , R ) , c a l ( L , R , m i d ) } solve(L,R) = min\{solve(L, mid), solve(mid+1, R), cal(L, R, mid)\}
  方程中为求解区间[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;
}

来源

邓俊辉老师的《算法训练营》第一期。

猜你喜欢

转载自blog.csdn.net/EasonDongH/article/details/85232238