详解平面点集最近点对问题
设平面上有点集 P P P,包含n个点,我们希望在这n个点中找到一对点,使其相互之间的距离最短。
如果采用蛮力法,即计算每一对点的距离之后,比较所有点对的距离。蛮力法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
那么,能不能找到一种时间复杂度更低的方法?这里,分治策略会让时间复杂度降低到 O ( n log 2 n ) O(n\log^2n) O(nlog2n)。
- 分(Divide):将点集P依据横坐标分成左右两个子集, P L P_L PL和 P R P_R PR
- 治(Conquer):分别计算 P L P_L PL和 P R P_R PR两个点集的最近点对 k 1 k_1 k1, k 2 k_2 k2
- 合(Combine):考虑
P
L
P_L
PL和
P
R
P_R
PR两个点集交界处的最近点对,即交界处可能有比
k
1
k_1
k1和
k
2
k_2
k2距离更近的两个点
显然,分和治的部分比较容易实现,本文将着重讨论“合”的实现。令 k = min ( k 1 , k 2 ) k=\min(k_1,k_2) k=min(k1,k2),因为我们的目标是寻找是否在边界线周围是否有距离小于 k k k的点对,所以我们在边界线左右分别划分一个宽为 k k k的区域,仅考虑在这个范围内的点(下图红色区域)。
划分完边界附近的区域后,我们开始考虑“合”,即寻找比 k 1 k_1 k1和 k 2 k_2 k2距离更近的两个点。最容易的想到的是,通过蛮力算法,我们把红色区域里的每一对点的距离全部计算出来,然后比较得到最短距离。但是这种方法的时间复杂度是 O ( n 2 ) O(n^2) O(n2),并没有实质上降低时间复杂度。
蛮力算法的时间复杂度为什么是 O ( n 2 ) O(n^2) O(n2)?是因为我们将每一个点与红色区域里的所有点计算距离,所以我们改进算法的思路是,能不能让每一个点仅与有限个点计算距离,从而使时间复杂度降低到 O ( n ) O(n) O(n)?
现在我们考虑一个问题,对于下图中的红色区域,由上述划分可知,每个正方形区域中任意两点之间的距离不小于 k k k,那么两个正方形区域中最多可以容纳多少点?
如下图所示,最多只能容纳8个点,8个点分布在两个正方形的四角。
也就是说,在沿纵轴长为 k k k的划分的红色区域内,最多有八个点,同时,因为我们的目标是找到跨越边界线的、距离小于 k k k的两点,所以跨越边界线的两点之间纵坐标的差值小于等于 k k k。基于这两点原因,我们可以很清晰地看到,一个点仅与纵坐标与它相近的7个点计算距离即可,没有必要与所有点之间计算距离。我们可以得到如下设计思路:
- 将红色区域内的点按纵坐标从小到大排序
- 对于区域内的每一个点,只计算它与序号在它之后的7个点之间的距离并与当前最短距离比较
我们分析一下上述算法的时间复杂度:
- 按横坐标排序 P P P中的点需要 O ( n log n ) O(n\log n) O(nlogn)时间,注意这个排序是预排序
- 由于已经排序过,划分子集需要 Θ ( 1 ) \Theta(1) Θ(1)时间
- “合”中,排序花费 O ( n log n ) O(n\log n) O(nlogn)时间,每个点最多进行7次距离计算和比较,最坏情况下耗费 Θ ( n l o g n ) \Theta(nlogn) Θ(nlogn)
KaTeX parse error: Unknown column alignment: * at position 43: … \begin{array}{*̲*lr**} …
上述递推式的解为 T ( n ) = O ( n log 2 n ) T(n)=O(n\log^2n) T(n)=O(nlog2n)。每次对子问题都要进行一遍排序严重影响到时间复杂度。我们可以空间换时间,先将点集中的元素以它们的纵坐标进行一遍排序然后存储在 Y Y Y数组中,在“合”步骤中仅需在 Θ ( n ) \Theta(n) Θ(n)时间内提取并比较,这样递推式变为:
KaTeX parse error: Unknown column alignment: * at position 43: … \begin{array}{*̲*lr**} …
递推式的解为 T ( n ) = Θ ( n log n ) T(n)=\Theta(n\log n) T(n)=Θ(nlogn)。
下面给出整个算法的伪代码:
1.以x坐标增序对P中点排序
2.Y:=以y坐标增序对P中点排序
3.k:=cp(1,n)
cp(low,high)
if((high-low+1)<=3)
直接用蛮力算法计算k
else
mid:=(low+high)/2
x0:=x(P[mid])
k1:=cp(low,mid)
k2:=cp(mid+1,high)
k:=min{k1,k2}
t:=0
for i:=low to high //从Y中抽取T,T表示红色区域
if(|x(Y[i])-x0|<=k)
t:=t+1
T[t]:=Y[i]
k':=2k //把k'初始化为比k大的值
for i:=1 to t-1
for j:=i+1 to min{i+7,t}
if(d(T[I],T[j])<k')
k':=d(T[I],T[j])
k:=min{k,k'}