题解 P7883 【平面最近点对(加强加强版)】
囧仙
2021-10-01 18:04:15
## 题目大意
> 给定平面上 $n$ 个点,求出两点间欧几里得距离的最小值。
## 题解
由于本题是模板题,因此本题解**尽量详细**地讲述怎么使用**分治法**计算出平面最近点对的距离。一些奇技淫巧(比如随机旋转坐标系什么的),这里就不提了。
---
容易发现,坐标系上这些点的顺序并不会影响到最终的求解。因此可以先将所有的点**按照 $\bm x$ 坐标**从小到大排序(如果两个点 $x$ 坐标相同,那就随便排了)。接着使用分治法。
分治法的基本思路是,**将大问题转化为类似的小问题,解决小问题后通过合并解决大问题**。在本题中,我们要解决的问题就是计算标号为 $0,1,2\cdots (n-1)$ 的这 $n$ 个点的两点间最近距离。我们设计算标号为 $l,l+1,\cdots (r-1)$ 的点的两点间最近距离为 $\mathrm{Solve}(l,r)$,本题即求 $\mathrm{Solve}(0,n)$。要想解决 $\mathrm{Solve}(l,r)$,可以考虑先解决 $\mathrm{Solve}\left(l,\left\lfloor\frac{l+r}{2}\right\rfloor\right)$ 和 $\mathrm{Solve}\left(\left\lfloor\frac{l+r}{2}\right\rfloor,r\right)$。
![](https://cdn.luogu.com.cn/upload/image_hosting/5tqycq1x.png)
容易发现,当 $r=l+1$ 时不需要做任何处理(此时返回 $ans=+\infty$)。关键问题在于,**如何合并两个子问题的结果**。
这里生成了 $9$ 个点作为例子:
![](https://cdn.luogu.com.cn/upload/image_hosting/zord2dmu.png)
按照 $x$ 坐标排序后,从左往右依次为 $P_1,P_7,P_3,P_2,P_8,P_4,P_9,P_5,P_6$。将 $\mathrm{Solve}(0,9)$ 转化为 $\mathrm{Solve}(0,4)$ 和 $\mathrm{Solve}(4,9)$:
![](https://cdn.luogu.com.cn/upload/image_hosting/a1qy8l03.png)
> 注意,这里使用的是左闭右开区间。因此 $P_8$ 应该在蓝色部分,而非红色部分。
对于 $\mathrm{Solve}(0,4)$,可以进一步划分区间:
![](https://cdn.luogu.com.cn/upload/image_hosting/dq13vwcl.png)
当红色部分被划分成了绿色部分和红色部分后,可以发现这两个子问题内都只有两个点了。因此直接更新最短距离 $d\gets\min(d,\operatorname{dis}(P_1,P_7)),d\gets\min(d,\operatorname{dis}(P_2,P_3))$,此时 $d=\sqrt{5}$。
下面考虑如何将绿色部分和红色部分的结果进行合并。也就是计算**左边的点**和**右边的点**相连对答案进行的更新。
![](https://cdn.luogu.com.cn/upload/image_hosting/skrxsikn.png)
将距离紫红色线段不超过 $d$ 的点**全部**添加到当前需要处理的点内(显然,如果某个点到紫红色线段的距离已经超过了 $d$,那么它到另一侧节点的距离肯定是超过 $d$ 的。可以剪枝剪去)。**按照 $\bm y$ 坐标从小到大**考虑这些点之间产生的贡献。图中的绿色部分就是 $P_1$ 需要考虑的点(对于 $P_1$ 来说,$y$ 坐标和它相差大于 $d$ 的点也不用考虑了)。当然了,此时的绿色部分内没有任何的点,因此 $P_1$ 在这一部分不会对答案进行更新。
![](https://cdn.luogu.com.cn/upload/image_hosting/uzssi49v.png)
处理完 $P_1$,就该处理 $P_3$ 了。此时 $P_2$ 进入到了绿色区域内。更新答案:$d\gets\min(d,\operatorname{dis}(P_2,P_3))$。
![](https://cdn.luogu.com.cn/upload/image_hosting/chy1ne93.png)
接着是处理 $P_2$。更新答案:$d\gets\min(d,\operatorname{dis}(P_2,P_7))$。
此时红色部分被完全处理完了。$d=\sqrt 5$。在上述过程中,我们暴力添加了所有到紫红色线段不超过 $d$ 的点,并且按照 $y$ 坐标从小到大进行处理。每个点的考虑范围就是 $y$ 坐标到它不超过 $d$ 的绿色矩形(上述例子没有出现绿色矩形内有多个点的情况。如果出现了多个点,需要依次连边进行答案的更新)。
![](https://cdn.luogu.com.cn/upload/image_hosting/uqp6uhdg.png)
处理完红色部分,再处理完蓝色部分,最后将橙色线段两侧距离不超过 $d$ 的点按照 $y$ 坐标排序后进行处理。最终得到答案为 $2$。
看上去这个做法在合并时是非常暴力的(因为一旦绿色部分内的点数非常多,就会导致复杂度退化)。但是可以证明,绿色部分内的点不超过 $5$ 个:
![](https://cdn.luogu.com.cn/upload/image_hosting/hgoewmnv.png)
考虑这样的一张图。容易发现,不可能会有某个点落入圆内,否则在合并之前求得的 $d$ 会变得更小。在圆的四个角落内的点又最多只有 $4$ 个,因此绿色部分的点的数目不超过 $5$。
现在假设我们已经给 $[l,r)$ 内的点以 $y$ 坐标为关键字排好了序。我们需要依次计算每个点对应的的绿色区域内有哪些点。容易发现,随着 $y$ 坐标的升高,每次只会是绿色区域上面的点加入,只会是绿色区域底部的节点移出。因此可以使用双指针解决这种问题。
双指针对应了这样一种思想:
![](https://cdn.luogu.com.cn/upload/image_hosting/f4myiuzr.png)
指针 $L$ 和指针 $R$ 之间是需要处理的数据。每次将 $L$ 指针向右移动后,将 $R$ 指针向右移动,直到拓展到需要处理的数据的右边界。容易发现,$L$ 和 $R$ 都只会移动最多 $n$ 次,因此维护待处理数据的时间复杂度是 $\mathcal O(n)$ 的。
考虑计算整个算法的时间复杂度。
设 $T(n)$ 为解决 $n$ 个点的时间复杂度。那么有 $T(n)=2T(n/2)+n\log n$($2T(n)$ 是分治解决子问题产生的代价;$n\log n$ 则是按照 $y$ 坐标排序;此外还需要使用双指针 $\mathcal O(n)$ 地维护待处理数据;每个节点最多会与 $5$ 个节点相连进行更新,因此这部分复杂度还是 $\mathcal O(n)$)。有:
$$\begin{aligned}
T(n)&=2T(n/2)+n\log n\cr &=4T(n/4)+n\log n+2(n/2)\log(n/2)\cr
&=4T(n/4)+2n\log n \cr
&=8T(n/8)+3n\log n \cr
&=\cdots \cr
&=n\log^2n
\end{aligned}$$
瓶颈在于每次合并答案时进行的排序。同样是分治思想,可以使用归并排序,在左右子区间的节点都排好序后,$\mathcal O(n)$ 地合并两个区间。此时总时间复杂度就是 $\mathcal O(n\log n)$ 了。
## 参考代码
```cpp
#include<bits/stdc++.h>
#define up(l,r,i) for(int i=l,END##i=r;i<=END##i;++i)
#define dn(r,l,i) for(int i=r,END##i=l;i>=END##i;--i)
using namespace std;
typedef long long i64;
const int INF =2147483647;
struct Point{int x,y;};
typedef vector<Point>::iterator Iter;
bool cmpx(const Point a,const Point b){return a.x<b.x;}
bool cmpy(const Point a,const Point b){return a.y<b.y;}
double dis(const Point a,const Point b){
return sqrt(pow(a.x-b.x,2)+pow(a.y-b.y,2));
}
void slv(const Iter l,const Iter r,double &d){
if(r-l<=1) return;
vector<Point> Q; Iter t=l+(r-l)/2;double w=t->x;
slv(l,t,d),slv(t,r,d),inplace_merge(l,t,r,cmpy);
for(Iter x=l;x!=r;++x)
if(abs(w-x->x)<=d) Q.push_back(*x);
for(Iter x=Q.begin(),y=x;x!=Q.end();++x){
while(y!=Q.end()&&y->y<=x->y+d) ++y;
for(Iter z=x+1;z!=y;++z) d=min(d,dis(*x,*z));
}
}
vector<Point> X; int n; double ans=1e18;
int qrd(){
int w=1,c,ret;
while((c=getchar())> '9'||c< '0') w=(c=='-'?-1:1); ret=c-'0';
while((c=getchar())>='0'&&c<='9') ret=ret*10+c-'0';
return ret*w;
}
int main(){
n=qrd(); up(0,n-1,i){
int x=qrd(),y=qrd(); X.push_back({x,y});
}
sort(X.begin(),X.end(),cmpx),slv(X.begin(),X.end(),ans);
printf("%.0lf\n",ans*ans);
return 0;
}
```