问题描述
对于平面上给定的N个点,给出所有点对的最短距离,即,输入是平面上的N个点,输出是N点中具有最短距离的两点。
暴力法
暴力法的思路相对简单,即枚举所有可能的配对情况,然后一一比对,找到距离最小的点,一共有 Cn2 种组合,也就是 n*(n-1)/2种,总的时间复杂度是O(n^2),因为时间开销过于昂贵,我们寻求别的方法
分治法
问题分割策略:排序,取中位数
如果像快速排序,分割的策略与数据有关,那么算法便会退化,不能达到稳定的O(nlogn)
但是如果效仿归并排序,每次选取中点(中位数)进行分割,那么可以使得递归树的深度稳定的控制在log(n)
这里对点做预处理,按照x升序,x相同则y升序排序,这样我们每次选取中间下标的点,都能尽量地将点分割成几何上的两半
子问题存在分析:
问题是选取两个点,p1,p2,使得他们距离最短,子问题存在于以下空间
- p1 p2 同时位于左边点集
- p1 p2 同时位于右边点集
- p1 p2 一个在左侧一个在右侧
对于子问题1和2,我们可以通过递归直接解决,难点就在于解决子问题3,即两点在两边的情况
子问题3求解 缩小空间:
想起来归并是要【合并】的,如何有效利用递归得到的答案呢?
注意这里如果要一一配对找两边点求距离,那么合并效率还是O(n^2)
递归得到两边点子集的最近点对距离,取最小值d,我们可以借此距离d来缩小我们查找的空间
从中间点向外扩散,如果点与中间点的x左边之差,大于d,那么这个点及其往后的点,都不可能是最优的答案,缩小了查找空间
缩小查找空间,其实是不稳定的,可能查找空间和原来一样,所以我们不能暴力枚举查找空间内的点,下面引入一个结论,来帮助我们快速查找
结论:点目标出没区域确定
结论:对于左边区域的每一点p,要想在右边区域找到一点p’使得 pp’距离<=d,这样的点p’必定存在于【以p的y座标为中心,上下延展d形成的d*2d的子矩形中】
证明:
假设极限情况,p在左右边线上,以p为中心半径为d画圆,半圆区域内,都是距p的距离小于d的,半圆是矩形的真子集,故目标点一定存在于矩形中
而且因为右侧的点具有稀疏性,即两两之间距离大于等于d, d*2d
的矩形区域,矩形区域内最多存在6个点(如果圆形区域则是四个)
我们遍历左边区域的所有点,划分出d*2d
的矩形区域,然后检查6个点,更新答案即可,可是如何用短时间找到d*2d
的矩形区域? 想到矩形区域的划分和y有关,那么应该是利用y座标的特性了!
y的划分
对左右的子区域的所有点按照y升序排序(左右分开排序,不是混合在一起),在指针i
升序遍历左边的所有点的时候,设置指针h
在右边的点中向后查找,找到第一个不矮于i
点y
座标d
高度的点h
(即h
和i
点高度差小于d
),从h
往后找6个点即可
(因为点y座标递增,那么对应的矩形区域,下边界也递增,那么上一次找过的下界点(h指针),就不用再从头开始找了,直接在上一次的基础上找就行了)
合并代价O(n)的证明
因为左右点集都是升序,这表明,i++之后,下一个i
点的d*2d
矩形的下边界一定会提升,而h
指针随着做出更新即可,因为i点矩形的下边界是递增的,h指针不走回头路
h指针最多从0增加到右边区域点的个数,最多n/2次,所以遍历左边点集,h迭代的次数,均摊到每一次i迭代,就是均摊一次,O(1),而查找矩形区域总共是6个点,也是常数时间可以完成,所以总的复杂度仍然是O(n)
分治法的改进:
因为每次递归之后,还要对左右按d划分的区域排序,总复杂度是O(2*n/2log(n/2)), 即合并代价,不是线性效率,总体复杂度超过O(nlong(n))
那么如何优化呢?
效仿归并排序,每次二分递归之后,我们对数组归并,因为递归排好了左右子区间,这样归并后,数组就是对y有序的了,不用消耗额外的时间再排序了,可以使合并子问题的代价变为O(n)级别
代码
代码的前半部分是一些帮助函数,后面两个函数才是分治法和归并分治法的代码
#include <bits/stdc++.h>
using namespace std;
typedef struct p
{
int x, y;
p(){}
p(int X, int Y):x(X),y(Y){}
}p;
/*
浮点最小函数,防止默认min的int形参截断
*/
double lfmin(double a, double b)
{
return (a<b)?(a):(b);
}
/*
比较函数,排序用,x升序,x相同则y升序
param p1 : 第一个点
param p2 : 第二个点
return : p1 前于 p2 ?
*/
bool cmpx(const p& p1, const p& p2)
{
if(p1.x==p2.x) return p1.y<p2.y;
return p1.x<p2.x;
}
/*
比较函数,排序用,则y升序,归并用
param p1 : 第一个点
param p2 : 第二个点
return : p1 前于 p2 ?
*/
bool cmpy(const p& p1, const p& p2)
{
return p1.y<p2.y;
}
/*
求解两点欧氏距离
param p1 : 第一个点
param p2 : 第二个点
return : 距离,浮点数
*/
double dis(const p& p1, const p& p2)
{
return sqrt((double)(p1.x-p2.x)*(p1.x-p2.x)+(double)(p1.y-p2.y)*(p1.y-p2.y));
}
/*
求两点水平距离
param p1 : 第一个点
param p2 : 第二个点
return : 水平距离,浮点数
*/
double disX(const p& p1, const p& p2)
{
double ans = (double)p1.x - (double)p2.x;
if(ans<0) return ans*-1;
return ans;
}
/*
暴力求解最近点对
param points : 点的数组
return : 最近点对距离
*/
double cp(vector<p>& points)
{
double ans = (double)INT_MAX;
for(int i=0; i<points.size(); i++)
for(int j=i+1; j<points.size(); j++)
ans = lfmin(ans, dis(points[i], points[j]));
return ans;
}
/*
分治求解最近点对,复杂度O(nlog(n)log(n))
param points : 点的数组
param l : 点数组左端点
param r : 点数组右端点
return : 最近点对距离
explain : 区间[l, r]左闭右闭
*/
double cp(vector<p>& points, int l, int r)
{
if(l>=r) return (double)INT_MAX;
if(l+1==r) return dis(points[l], points[r]);
int mid=(l+r)/2, le=mid, ri=mid, h=0;
double d=lfmin(cp(points, l, mid), cp(points, mid+1, r)), ans=d;
vector<p> ll, rr;
while(le>=l && disX(points[mid], points[le])<=d) ll.push_back(points[le]), le--;
while(ri<=r && disX(points[mid], points[ri])<=d) rr.push_back(points[ri]), ri++;
sort(ll.begin(), ll.end(), cmpy), sort(rr.begin(), rr.end(), cmpy);
for(int i=0; i<ll.size(); i++)
{
while(h<rr.size() && rr[h].y+d<ll[i].y) h++; h=min((int)rr.size(), h);
for(int j=h; j<min((int)rr.size(), h+6); j++)
if(!eqfunc()(ll[i], rr[j])) ans=lfmin(ans, dis(ll[i], rr[j]));
}
return lfmin(ans, d);
}
/*
分治+归并求解最近点对,复杂度O(nlog(n))
param points : 点的数组
param l : 点数组左端点
param r : 点数组右端点
return : 最近点对距离
explain : 区间[l, r]左闭右闭
*/
double cpm(vector<p>& points, int l, int r)
{
if(l>=r) return (double)INT_MAX;
if(l+1==r)
{
if(cmpy(points[r], points[l])) swap(points[l], points[r]);
return dis(points[l], points[r]);
}
int mid=(l+r)/2, le=mid, ri=mid, h=0;
p midp = points[mid];
double d=lfmin(cpm(points, l, mid), cpm(points, mid+1, r)), ans=d;
inplace_merge(points.begin()+l, points.begin()+mid+1, points.begin()+r+1, cmpy);
vector<p> ll, rr;
for(int i=l; i<=r; i++)
{
if(midp.x>=points[i].x && disX(midp, points[i])<=d) ll.push_back(points[i]);
if(midp.x<=points[i].x && disX(midp, points[i])<=d) rr.push_back(points[i]);
}
for(int i=0; i<ll.size(); i++)
{
while(h<rr.size() && rr[h].y+d<ll[i].y) h++; h=min((int)rr.size(), h);
for(int j=h; j<min((int)rr.size(), h+6); j++)
if(!eqfunc()(ll[i], rr[j])) ans=lfmin(ans, dis(ll[i], rr[j]));
}
return lfmin(ans, d);
}