分治法(4):求最近点对

Last updated on May 5, 2025 pm

本文以“寻找平面上最近点对”问题为核心,探讨了如何设计和分析能解决该问题的分治算法。

让我们从研究一个“寻找平面上最近点对”的有趣问题。给定nn个点(x1,y1),(x2,y2),,(xn,yn)(x_1, y_1), (x_2, y_2), \ldots, (x_n, y_n),要求输出两个相距最近的点。

最直接的想法是暴力法,即计算所有n(n1)2\dfrac{n(n-1)}{2}个点对,并输出距离最小的。这种做法的时间复杂度为O(n2)O(n^2),因此我们应该比O(n2)O(n^2)做得更好。

那么,有什么方法可以改进呢?我们曾经说过,想要降低时间复杂度,我们必须看看我们是否做了冗余的操作。在这个问题中,就是
有没有点对可以不用比较,即不用计算距离,就可以确定不是最小的。

如果是人类完成这个事情,ta一定不会把所有点两两计算距离,那些看起来很远的点对就不会被计算。但是,计算机如何知道哪些点“看起来很远”呢?

从一维情况出发

二维的情况看起来有点复杂,让我们先从一维开始,即考虑所有点都在一条线上的情况(不妨假设所有点都在x轴上)。

一维情况示意图

明显可以看出,我们似乎只要比较相邻的点对即可,距离很远的点对是不需要比较的。因此,我们就有了通过排序来减少无用计算的思路。我们在一维情况下得到如下的算法,即先把所有点按x坐标排序,再比较相邻的点对。

一维情况算法设计

分治法框架

那么,我们如何把一维情况应用到二维呢?别忘了,我们有分治法

还记得,分治法的设计无非两件事:

  1. 怎么分:如何将原问题拆成规模更小的子问题
  2. 怎么治:子问题的解如何合并成原问题的解

首先,怎么分?延续一维情况的思路,我们可以

  • 先将所有点按x坐标排序(先假设所有点的x坐标不同),
  • 再画一条竖线,使得其两侧分别有n/2n/2个点。

分治法的划分示意图

通过递归,分治法会分别找到竖线两侧距离最近的点对。接下来,怎么治?也就是如何找到整张图中距离最近的点对?

与求逆序数类似,我们已经知道了竖线左侧最近的点对、竖线右侧最近的点对,我们还需要知道跨过竖线的最近点对(即一个点在竖线左侧,另一个点在竖线右侧)。再比较这三个最近点对的距离,输出最小的即可。

分治法的合并示意图

寻找竖线两侧的最近点对

现在唯一的问题就是,如何找到跨过竖线的最近点对

最直接的方法,就是比较所有跨过竖线的(n2)2\left(\dfrac{n}{2}\right)^2个点对。但不用我多说,你们一定知道这会让算法的时间复杂度回到O(n2)O(n^2),因为只是换了比较顺序,并没有减少任何比较。不信的话,下图中进行了时间复杂度分析。

朴素分治法的时间复杂度分析

并且我们可以看到,确实是合并操作的O(n2)O(n^2)主导了时间复杂度,排序的O(nlogn)O(n \log n)并没有影响。因此,我们可以保留排序。

第1个聪明想法——只看2δ\delta条状区域

想要减少时间复杂度,就要减少冗余操作。那么,有什么操作是冗余的呢?

直观理解不需要比较的点对

与一维情况一致,核心想法是,我们不用计算所有点对的距离,即忽略相隔太远的点对。目前,所有点已经按x轴坐标排序,所以我们已经掌握了一些x方向的信息,可以尝试先筛选掉x方向距离过大的点

为了方便起见,我们记:

  • 竖线左侧最近点对的距离为δL\delta_L
  • 竖线右侧最近点对的距离为δR\delta_R
  • δ=min{δL,δR}\delta = \min\{\delta_L, \delta_R\}

容易想到,x方向距离超过δ\bm{\delta}的点对是不需要计算的,因为他们的距离一定超过了目前的最小距离。

既然如此,我们在原先竖线(即中线)的两侧各 δ\bm{\delta} 处,再分别画一条竖线。这样,落在两条竖线外侧的点,不需要参与跨过中线的最近点对的计算,因为他们和对面任何一个点的x方向距离就已经超过δ\delta。所以,我们只需要关注两条竖线之内(即2δ\bm{2\delta}条状区域内)的点

第1个聪明想法

第2个聪明想法——只看2δ×δ2\delta \times \delta矩形

那么,现在我们的问题转化为,如何找到2δ\bm{2\delta}条状区域中跨过中线的最近点对

条状区域示意图

最直接的还是暴力法,也就是计算该区域内所有点对的距离。假设该区域内共有mm个点,那么暴力法的时间复杂度为O(m2)O(m^2)。可惜的是,我们并不能知道mm的大小。如果m=nm = n,即所有点都挤在这个2δ2\delta条状区域中,时间复杂度又会回到O(n2)O(n^2)。看来,我们还需要更巧妙的方法。

观察可以发现,对于y方向上距离很远的点,我们也可以不用计算。(这是容易理解的,因为我们刚才只对x方向作了限制,并没有处理过y方向。)具体来说,y方向距离超过δ\bm{\delta}的点对不需要计算

第2个聪明想法

具体来说,我们先固定一个点aa,在aa的上方寻找另一个点bb。那么,比aa的y坐标大超过δ\delta的点bb都可以不考虑,因为(a,b)(a, b)间的距离一定超过了δ\delta。也就是说,我们只需要在如图所示的 2δ×δ\bm{2\delta \times \delta} 矩形中寻找点 b\bm{b}

我们限制点bbaa的上方,是为了避免重复计算。

那么,现在的时间复杂度能降低了吗?回忆一下,为什么上一次我们没能成功。那是因为2δ2\delta条状区域中点的数量无法控制,仍有可能有nn个点,导致比较次数无法减少。

那么,这次的2δ×δ2\delta \times \delta矩形区域中点的数量可以控制了吗?也就是说,会不会再出现很多点挤在矩形中的情况呢?

想象的矩形区域中点的数量示意图

直观上看,是不会的。因为δ\delta已经是中线两侧的点对的最短距离,不可能在2δ×δ2\delta \times \delta的矩形中塞下很多个距离不小于δ\delta的点。

这其实可以抽象成一个数学问题。我们不妨只关注左侧边长为 δ\bm{\delta} 的正方形,研究其中最多能存在几个两两距离不小于δ\bm{\delta}的点

我们可以将边长为δ\delta的正方形划分为四个边长δ/2\delta/2的小正方形。每个小正方形中的最大距离为对角线δ/2<δ\delta / \sqrt{2} < \delta,因此最多只能放一个点。也就是说,整个大正方形最多存在4个点

矩形区域中点的数量限制的证明示意图

同理,右侧正方形也最多有四个点,从而整个 2δ×δ\bm{2\delta \times \delta} 的矩形中最多存在8个点(包括固定的点aa)。也就是说,a\bm{a}最多只要和它上方的7个点计算距离

真实的矩形区域中点的数量示意图

完整算法及时间复杂度分析

至此,我们得到了求最近点对的完整算法

求最近点对算法

  1. 将所有点按x坐标排序
  2. 画一条竖线ll使每侧有n/2n/2个点
  3. 分别递归地找到竖线两侧的最近点对,设最小距离分别为δL\delta_LδR\delta_R
  4. δ=min{δL,δR}\delta = \min\{\delta_L, \delta_R\},设SS'是离ll距离不超过δ\delta的点的集合
  5. SS'按y坐标排序
  6. SS'中每一个点aa,检查SS'aa上方的7个点,找到最近点对
  7. 返回第3步和第6步中距离最小的点对

时间复杂度分析

该算法的时间复杂度分析如下。

求最近点对算法设计及时间复杂度分析

因此,我们有

T(n)=2T(n2)+O(nlogn)T(n) = 2T\left(\frac{n}{2}\right) + O(n \log n)

注意,这里我们无法使用主定理。但可以通过数学归纳法或展开T(n)\bm{T(n)}证明,

T(n)=O(nlog2n)T(n) = O(n \log^2 n)

事实上,我们有更一般化的主定理:设 T(n)=aT(n/b)+ndlogwnT(n) = aT(n/b) + n^d \log^w n,那么

T(n)={O(ndlogwn)if a<bd,O(nlogba)if a>bd,O(ndlogw+1n)if a=bd.T(n) = \begin{cases} O(n^d \log^w n) & \text{if } a < b^d, \\ O(n^{\log_b a}) & \text{if } a > b^d, \\ O(n^d \log^{w+1} n) & \text{if } a = b^d.\end{cases}

算法优化思路

其实我们的算法可以进一步优化到O(nlogn)\bm{O(n \log n)}。根据主定理,如果T(n)=2T(n/2)+O(n)T(n) = 2T(n/2) + O(n),那么有T(n)=O(nlogn)T(n) = O(n \log n)。因此,想要将算法的时间复杂度降到O(nlogn)O(n \log n),我们必须把递归函数中的两个O(nlogn)O(n \log n)的排序去掉(即上图红线部分)。也就是说,我们需要把划分和合并的代价都降到O(n)\bm{O(n)}

注意,我们不可能在递归函数中保留排序,因为排序(基于比较)的最低时间复杂度为O(nlogn)O(n \log n)

x坐标排序优化

如何优化划分中的按x坐标排序?事实上,我们并不需要每次都排序,因为x坐标排序的作用仅仅是寻找中线

因此,我们只要在算法一开始做一次x坐标排序,就可以确定全部中线的位置。在第1层递归时,使用n2\dfrac{n}{2}处的中线;在第2层递归时,分别使用n4\dfrac{n}{4}3n4\dfrac{3n}{4}处的中线;以此类推。

这样,我们就将x坐标排序移到了递归函数外侧,这使得划分的时间复杂度降低到O(n)O(n)

y坐标排序优化

相比之下,优化合并时对y坐标排序稍显困难,因为我们需要知道找到每个点上方的7个点。想要避免每次合并时排序,有以下两种思路。

  1. 和x坐标排序优化类似,在算法一开始对y坐标排序,并使用该序列进行递归。这样,每次递归传入的序列都在y坐标上有序,从而在每次选出SS'后,其中的点已经按照y坐标排序好,不需要再排序。

  2. 和求逆序数算法类似,在找最近点对的同时做y坐标的归并排序。这样,只需要用O(n)O(n)的时间将左右分别按y坐标排好的点做一次合并,即可得到按y坐标排序的结果。

结合这两个优化,我们就将划分和合并的时间复杂度降到O(n)O(n),从而算法整体优化到O(nlogn)O(n \log n)

注:本文中所有图片均来自张宇昊老师的课程PPT。


分治法(4):求最近点对
https://cny123222.github.io/2025/03/11/分治法-4-:求最近点对/
Author
Nuoyan Chen
Posted on
March 11, 2025
Licensed under