最邻近点对问题(Closest-Pair Problem):二维的分治解法详解

这是该系列的第二篇。一维、三维的最邻近点对问题(Closest-Pair Problem):戳这里
PS:建议先快速浏览一维问题的分治解法。因为各维度的解决思路具有高度的关联性。

1 问题描述

现在二维下,点不再是线性分布了。对于两个二维点pi=(xi,yi) p_i = (x_{i}, y_{i})p
i

=(x
i

,y
i

)和pj=(xj,yj) p_j = (x_{j}, y_{j})p
j

=(x
j

,y
j

),它们之间的欧几里得距离为(xi−xj)2+(yi−yj)2−−−−−−−−−−−−−−−−−√ \sqrt{(x_i - x_j)^2 + (y_i - y_j)^2}
(x
i

−x
j

)
2
+(y
i

−y
j

)
2


暴力算法只能枚举每个不同的点对。它的时间复杂度是O(n2) O(n^2)O(n
2
)。

但是我们可以沿用一维的思维模式来解决这个问题。并且这时,由于排序也需要O(nlogn) O(nlogn)O(nlogn),按照分治思想可以把暴力法的复杂度降到O(nlogn) O(nlogn)O(nlogn)。

2 算法描述


2.1 开始前

首先对于一个乱序的点集P PP,我们需要分别对x xx和y yy进行排序,得到Px P_xP
x

和Py P_yP
y

(注意是分别,而不是先后)。前者用于divide,后者用于merge。

2.2 Divide步骤

▲ 第一步:拆分

对于任意的点集P PP,其中点的数量∣P∣=n \vert P\vert = n∣P∣=n,类似于一维情况,可以Px P_xP
x

中第⌊n/2⌋ \lfloor n/2\rfloor⌊n/2⌋个点的坐标(记为x∗ x^*x

)处,将其分为Q QQ区与R RR区。

准确来说,分区的标准是点在Px P_xP
x

中的位置,小于等于⌊n/2⌋ \lfloor n/2\rfloor⌊n/2⌋划入Q QQ区,反之R RR区。不过按x∗ x^*x

来理解会比较方便。

▲ 第二步: 维护有序数组

对于Q QQ区和R RR区,我们想要维护4个有序数组:按x xx或y yy排好序的Qx Q_xQ
x

, Qy Q_yQ
y

, Rx R_xR
x

,Ry R_yR
y

。其中,按x xx排序的数组,Qx Q_xQ
x

和Rx R_xR
x

,按索引就可以分开了。

而Qy Q_yQ
y

和Ry R_yR
y

,需要新建数组,顺次遍历Py P_yP
y

并判断从属的区域来构建,花费O(n) O(n)O(n)时间,且它们自然就是有序的。注意,如果你想通过Qx Q_xQ
x

和Rx R_xR
x

重排序得到,会影响整个算法的复杂度。

现在原问题就变成了两个完全相同的子问题。最终只有小于等于3个点时,可以直接比较每个点对得出答案。

2.3 Merge步骤

假设Q QQ区找到了局部最邻近点{q1,q2} \{q_1, q_2\}{q
1

,q
2

},距离为δ1 \delta_1δ
1

,同理R RR区找到了{r1,r2} \{r_1, r_2\}{r
1

,r
2

},距离为δ2 \delta_2δ
2

。并且在所有被分割线割离的点对中,即所有点对 {pi,pj} \{p_{i}, p_{j}\}{p
i

,p
j

} 且 pi∈Q∧pj∈R p_i \in Q \wedge p_j \in Rp
i

∈Q∧p
j

∈R,有一最小距离δ3 \delta_3δ
3

。现在对于原问题的解,有完全相同于一维的3种情况:

最小距离的点对在被分割线割离的点对集合中,即δ3 \delta_3δ
3

是最小,应返回对应被割离的点对。
Q QQ区中找到的点对距离最小,应返回{q1,q2} \{q_1, q_2\}{q
1

,q
2

}。
R RR区中找到的点对距离最小,应返回{r1,r2} \{r_1, r_2\}{r
1

,r
2

}。
现在算法的关键点就在于找出δ3 \delta_3δ
3

。一维情况被割离的点对中,需要比较的只有一个,而二维情况则大不一样,需要枚举两个区域的点的所有组合来比较,每次Merge时都将消耗O(n2) O(n^2)O(n
2
)的时间。

▲ 能否利用δ1 \delta_1δ
1

与δ2 \delta_2δ
2

,减少寻找δ3 \delta_3δ
3

的开销呢?

毕竟,我们只需要找可能比Q QQ和R RR中局部最近更近的点对。如果令δ=min(δ1,δ2) \delta = min(\delta_1, \delta_2)δ=min(δ
1


2

),把x∗ x^*x

左右宽为δ \deltaδ的区域记为S SS区,那么我们只需要检查这个区域内的点即可。


遗憾的是,这样做并没有减少算法的开销上限。 如果所有点都集中在这个区域里,那么仍然不可避免检查所有点对。这时,就要用到我们一直维护的Py P_yP
y

了。


假设我们要检查点s ss,它属于Q QQ区。以它为起点,取出S SS区域中高为δ \deltaδ的一块区域。然后如上图所示,我们可以把这块δ×2δ \delta \times 2\deltaδ×2δ的区域划分为8个box。一个box内最远距离是对角线距离δ/2–√<δ \delta/\sqrt2 < \deltaδ/
2

<δ,这意味着每个box中至多存在一点。那么根据鸽巢原理(Pigeonhole Principle),对于点s ss,仅需要检查它与比它稍大的7个点之间的距离即可。超出7个点后,距离一定会超过这块区域的高度δ \deltaδ,因而可以不用考虑。(为什么仅检查稍大的部分?原因是遍历时,稍小的部分已经检查过了)

▲ 具体做法:

从小到大遍历有序的Py P_yP
y

,如果点在距x∗ x^*x

小于δ \deltaδ的区域内,则将它加入S SS,这样S SS也是有序的。遍历S SS,每次只比较当前点和在其之上的7个点之间的距离。这个过程需要7n=O(n) 7n = O(n)7n=O(n)的时间。

PS:如果分别构造了S1 S_1S
1

和S2 S_2S
2

(三维中必须要这么做),那么还需要额外记录每一个点在另一半区域中的参考位置。这样,就可以仅比较对应的半个区域内的4个点来寻找δ3 \delta_3δ
3

3 算法分析

Divide步骤的拆分和维护是O(n) O(n)O(n)的,Merge步骤也是O(n) O(n)O(n)的,易知消耗的递推公式为:

fn={2fn/2+O(n)O(1)n>3n≤3 f_n=\left\{ \begin{aligned} &2f_{n/2} + O(n) & n>3\\ &O(1) & n\le 3 \end{aligned} \right.
f
n

={

https://www.dianyuan.com/people/838763
https://www.dianyuan.com/people/838764


2f
n/2

+O(n)
O(1)

https://www.dianyuan.com/people/838761
https://www.dianyuan.com/people/838762


n>3
n≤3

由主定理可知总消耗为O(nlogn) O(nlogn)O(nlogn)。排序消耗O(nlogn) O(nlogn)O(nlogn),则整个算法也是O(nlogn) O(nlogn)O(nlogn)。

4 伪代码

这里贴上伪码,重在理解。源码请移步 文末链接 下一节。


5 Java实现

(算了,我发现手机上我自己都不会去点连接看源码,还是放这里了好了,长就长点)
_(:з」∠)_

Point2.java

package util;

import java.util.Objects;

public class Point2 {
public int idx;
public long x, y;

public Point2(long x, long y) {
this.x = x;
this.y = y;
}

/**
* Check whether the first point is smaller in lexicographical order
*/
public static boolean smaller(Point2 p1, Point2 p2) {
if (p1.x == p2.x) {
return p1.y < p2.y;
}
return p1.x < p2.x;
}

/**
* Get distance of two points
*/
public static double getDis(Point2 p1, Point2 p2) {
long tmp1 = p1.x - p2.x;
long tmp2 = p1.y - p2.y;
return Math.sqrt(tmp1 * tmp1 + tmp2 * tmp2);
}

@Override
public String toString() {
return "Point{" +
"idx=" + idx +
", x=" + x +
", y=" + y +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point2 point2 = (Point2) o;
return x == point2.x &&
y == point2.y;
}

@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

https://www.dianyuan.com/people/838754
https://www.dianyuan.com/people/838755
https://www.dianyuan.com/people/838756
https://www.dianyuan.com/people/838757
https://www.dianyuan.com/people/838758
https://www.dianyuan.com/people/838759
https://www.dianyuan.com/people/838760

Dim2.java

package solve;

import util.Point2;

import java.util.Arrays;
import java.util.Comparator;

import static util.Point2.getDis;

public class Dim2 {
private Point2[] px, py;
private Point2[] sy; // save sy to reuse it in all sub procedure

/**
* Solve the problem
* @param points the point set
* @return the point pair with smallest distance
*/
public Point2[] solve(Point2[] points) {
int n = points.length;

/* before start searching */

px = new Point2[n];
py = new Point2[n];
System.arraycopy(points, 0, px, 0, n);
System.arraycopy(points, 0, py, 0, n);
Arrays.sort(px, Comparator.comparingLong(o -> o.x));
Arrays.sort(py, Comparator.comparingLong(o -> o.y));

sy = new Point2[n]; // the point in S, ranked by y

// record the point index in px
for (int i = 0; i < n; i++) {
px[i].idx = i;
}

/* Search the ans */

Point2[] res = find(0, n - 1, py);

/* output the result by lexicographical order */

if (Point2.smaller(res[0], res[1])) {
return new Point2[]{res[0], res[1]};
} else {
return new Point2[]{res[1], res[0]};
}
}

/**
* Find the pair with smallest distance
* @param x1 the left bound of px to find
* @param x2 the right bound of px to find (include)
* @param py the arr px[x1:x2] ranked by y
* @return the pair expressed in point index
*/
private Point2[] find(int x1, int x2, Point2[] py){
switch (x2 - x1 + 1) {
case 2:
return new Point2[]{px[x1], px[x2]};
case 3:
double dis12 = getDis(px[x1], px[x1 + 1]);
double dis23 = getDis(px[x1 + 1], px[x2]);
double dis13 = getDis(px[x1], px[x2]);
if (dis12 < dis23) {
if (dis12 < dis13) {
return new Point2[]{px[x1], px[x1 + 1]};
} else {
return new Point2[]{px[x1], px[x2]};
}
} else {
if (dis23 < dis13) {
return new Point2[]{px[x1 + 1], px[x2]};
} else {
return new Point2[]{px[x1], px[x2]};
}
}
}

/* Generate Qx, Rx, Qy, Ry */

int mi = (x1 + x2) / 2;
int idx1 = 0;
int idx2 = 0;
Point2[] qy = new Point2[mi - x1 + 1];
Point2[] ry = new Point2[x2 - mi];

for (Point2 p : py) {
if (p.idx <= mi) {
qy[idx1++] = p;
} else {
ry[idx2++] = p;
}
}

/* Search recursively */

Point2[] left = find(x1, mi, qy);
Point2[] right = find(mi + 1, x2, ry);

double dis1 = getDis(left[0], left[1]);
double dis3 = getDis(right[0], right[1]);
double delta = Math.min(dis1, dis3);

/* Find minimum distance in crossing-area pair */

// Generate S
long x = px[mi].x;
int cnt = 0;
for (Point2 p : py) {
if (x - delta <= p.x && p.x <= x + delta) {
sy[cnt++] = p;
}
}

Point2[] pairMin = new Point2[2];
double disMin = delta;

for (int i = 0; i < cnt; i++) {
Point2 syi = sy[i];
Point2 syj;

// check one side
for (int j = i + 1; j < cnt && j <= i + 7; j++) {
syj = sy[j];
double tmp = getDis(syi, syj);
if (disMin > tmp) {
disMin = tmp;
pairMin[0] = syi;
pairMin[1] = syj;
}
}
}

/* Compare and return */

if (pairMin[0] != null) {
return pairMin;
} else if (dis1 < dis3) {
return left;
} else {
return right;
}
}
}

————————————————
原文链接:https://blog.csdn.net/Carl_Rabbit/article/details/106840395

https://www.dianyuan.com/people/838732
https://www.dianyuan.com/people/838733
https://www.dianyuan.com/people/838734
https://www.dianyuan.com/people/838735
https://www.dianyuan.com/people/838736
https://www.dianyuan.com/people/838737
https://www.dianyuan.com/people/838738
https://www.dianyuan.com/people/838739
https://www.dianyuan.com/people/838740
https://www.dianyuan.com/people/838741
https://www.dianyuan.com/people/838742
https://www.dianyuan.com/people/838743
https://www.dianyuan.com/people/838744
https://www.dianyuan.com/people/838745
https://www.dianyuan.com/people/838746
https://www.dianyuan.com/people/838747
https://www.dianyuan.com/people/838748
https://www.dianyuan.com/people/838749
https://www.dianyuan.com/people/838750
https://www.dianyuan.com/people/838751
https://www.dianyuan.com/people/838752
https://www.dianyuan.com/people/838753

https://www.dianyuan.com/people/838765
https://www.dianyuan.com/people/838766
https://www.dianyuan.com/people/838767
https://www.dianyuan.com/people/838768
https://www.dianyuan.com/people/838769
https://www.dianyuan.com/people/838770
https://www.dianyuan.com/people/838771
https://www.dianyuan.com/people/838772
https://www.dianyuan.com/people/838773
https://www.dianyuan.com/people/838774
https://www.dianyuan.com/people/838775
https://www.dianyuan.com/people/838776
https://www.dianyuan.com/people/838777
https://www.dianyuan.com/people/838778
https://www.dianyuan.com/people/838779
https://www.dianyuan.com/people/838780
https://www.dianyuan.com/people/838781
https://www.dianyuan.com/people/838782
https://www.dianyuan.com/people/838783
https://www.dianyuan.com/people/838784
https://www.dianyuan.com/people/838785
https://www.dianyuan.com/people/838786
https://www.dianyuan.com/people/838787
https://www.dianyuan.com/people/838788
https://www.dianyuan.com/people/838789
https://www.dianyuan.com/people/838790
https://www.dianyuan.com/people/838791
https://www.dianyuan.com/people/838792
https://www.dianyuan.com/people/838793
https://www.dianyuan.com/people/838794
https://www.dianyuan.com/people/838795
https://www.dianyuan.com/people/838796
https://www.dianyuan.com/people/838797
https://www.dianyuan.com/people/838798
https://www.dianyuan.com/people/838799
https://www.dianyuan.com/people/838800
https://www.dianyuan.com/people/838801
https://www.dianyuan.com/people/838802
https://www.dianyuan.com/people/838803

猜你喜欢

转载自www.cnblogs.com/dasdfdfecvcx/p/13170641.html