最近对问题
目标是在一个给定的点集中找到距离最近的一对点。
解决最近对问题有两个常用的方法,一是蛮力法,二是本文记录的分治法。
分治法Python实现:
# -*- coding:utf-8 -*-
import math
def distance(p1, p2):
"""计算两个点之间的距离"""
return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)
def closest_pair(points):
"""最近对问题的实现"""
# 对点集按照x坐标排序
points.sort(key=lambda x: x[0])
# 递归终止条件:如果点集中只有一个或两个点,则直接返回
n = len(points)
if n == 1:
return None, None, math.inf
elif n == 2:
return points[0], points[1], distance(points[0], points[1])
# 分治求解
mid = n // 2
left_points = points[:mid]
right_points = points[mid:]
left_pair = closest_pair(left_points)
right_pair = closest_pair(right_points)
# 取左右子集中的最小距离
if left_pair[2] < right_pair[2]:
d = left_pair[2]
closest = left_pair
else:
d = right_pair[2]
closest = right_pair
# 考虑跨越左右子集的最近对
mid_x = points[mid][0]
# 在左边子集中找到横坐标最大的点
left_max_x = max((p for p in left_points if mid_x - p[0] < d), key=lambda x: x[0], default=None)
# 在右边子集中找到横坐标最小的点
right_min_x = min((p for p in right_points if p[0] - mid_x < d), key=lambda x: x[0], default=None)
if left_max_x is not None and right_min_x is not None:
# 如果左右子集中都存在点
# 计算横跨左右子集的距离,并更新最近对
mid_strip = [(x, y) for x, y in points if mid_x - d <= x <= mid_x + d]
closest_strip = closest_strip_pair(mid_strip, d)
if closest_strip[2] < closest[2]:
closest = closest_strip
return closest
def closest_strip_pair(points, d):
"""计算跨越左右子集的最近对"""
n = len(points)
min_distance = d
closest = None, None, min_distance
# 根据y坐标排序
points.sort(key=lambda x: x[1])
# 暴力枚举
for i in range(n):
for j in range(i+1, n):
if points[j][1] - points[i][1] > min_distance:
break
dist = distance(points[i], points[j])
if dist < min_distance:
closest = points[i], points[j], dist
min_distance = dist
return closest
# 示例:
# points = [(1, 2), (3, 5), (7, 1), (6, 8), (4, 3)]
points = [(2, 3), (12, 30), (40, 50), (5, 1), (12, 10), (3, 4)]
print(closest_pair(points))
运行结果:
((3, 5), (4, 3), 2.23606797749979)
((2, 3), (3, 4), 1.4142135623730951)
代码解释
- 在上面的代码中,首先定义了一个计算两个点之间距离的函数distance,就是初中学过的两个坐标之间的距离。
- 实现了一个最近对问题的函数closest_pair。在函数中,我们首先对点集按照x坐标排序,并根据点集大小分成左右两个子集。
- 然后,我们递归地在左右两个子集中分别求解最近对问题。接下来,我们计算左右子集中的最小距离,并记录最近对。
- 我们考虑跨越左右子集的最近对。如果跨越的最近对距离小于之前的最近对距离,则更新最近对。在计算跨越的最近对时,我们使用了一个辅助函数closest_strip_pair,它用于计算跨越左右子集的最近对。
- 最后,函数closest_pair返回最近对的坐标和距离。
总结思考
因为我本科就对算法课的学习不够深入,研究生阶段学起来有点吃力。
在这个分治法解决最近对问题时,将点按x升序排列,并在x//2处划分中线,此时分为两个子集,递归计算两个子集的各点对距离,并更新得到左右子集的最小距离Sl和Sr,到这里还比较好理解。
难点在于,最近点对一个在左子集,一个在右子集,这时需要根据d = min{Sl,Sr},在中线m左右各取长度为d的区域,再递归计算这个区域里边的所有点对距离并取最小和d进行比较。之所以在中线m左右各取长度d的区域里找,是因为,如果不在这个区域,那么就属于左子集或者右子集中的点对,而这部分已经被计算过了,最小距离为d。
借助课本中的图:
(a)图就是表示在(m-d,m+d)这个区域中递归求这些点对的最近距离并和左右子集中更小的最近距离作比较。
(b)图这个用了鸽笼原理得出最多不超过6个点,我也没太看明白,如果不在乎时间复杂度的话,直接对中间区域递归解决就可以,或许以后我会思考明白吧。
附上课本中的伪代码,日后再学习: