图谱连接

草稿

分治法求最近点对

结合递归和分割线附近的局部网格搜索,比枚举所有点对更快地找到最近的两个点。

algorithm intermediate geometryalgorithmsdivide-and-conquer

问题从哪里来

假设地图上有成千上万个点,你想找出距离最近的两个点。问题本身很简单:在所有点对中,找到欧氏距离(Euclidean distance)最小的一对。

找到最近的两个点开场只展示点云,暂时不揭晓最终点对。
AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

目标:最近点对要等合并过程推出

任务很容易说清楚:在所有点对中找到最近的一对。

第一个朴素想法

检查所有 n(n - 1) / 2 个点对,并记录目前见过的最短距离。

这个方法简单且正确,但需要 O(n^2) 次距离比较。还没有进入形式化算法,数量本身就已经是痛点。

枚举所有点对会变贵点对检查数量会按平方级增长。
n检查次数
510
20190
1000499,500

n(n - 1) / 2 次检查

1,000 个点时,枚举所有点对需要 499,500 次距离检查。

它为什么会痛

一个较小的当前最优距离,可以在比较之后排除某个远点对;但普通列表仍然没有便宜的方法去问:“哪些点近到值得检查?”

阈值还不是查找结构知道 r 可以在检查后排除远点对,但还不能直接找到近候选。
AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

普通列表不能便宜地查询附近点

缺少的操作是:只把 F 附近的点给我。

真正的合并问题是:左半边和右半边各自求完最近点对以后,还剩多少跨边界点对需要看?

核心发明

先按 x 坐标排序,用一条竖线把点集分成左右两半,递归求出两边内部的最近距离,再令 r 为两者的较小值。同侧点对已经由递归解决。

先分割,再复用已解半边递归已经在左边找到 E-F,在右边找到 G-H。
x=5E-FG-HAA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

左侧:E-F;右侧:G-H

合并阶段不会重新检查同侧点对。

小规模基本情况仍然用暴力法:当一个递归子问题最多只有三个点时,直接比较这些少量点对。递归的意义,是把难点变成一次合并问题。

基本情况供给合并小叶子用暴力法;合并阶段接收两个已解半边。
类别状态
左-左由左侧递归调用解决
右-右由右侧递归调用解决
跨边界只在合并阶段检查

n <= 3:暴力法

它先是分治算法,然后才用网格优化合并。

如果存在更好的答案,它一定是距离小于 r 的跨边界点对。这意味着两个端点都必须位于分割线两侧距离 r 以内。

只剩阈值带状区域更好的跨边界点对必须让两点都在分割线两侧距离 r 以内。
x=5AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

带内跨边界点对:E-G、E-H、F-G、F-H

带状区域过滤不可能的跨边界点对,但仍需要局部查找规则。

所有跨边界点对:36。带内点对:4。网格窗口点对:4。

带状区域只是过滤器。在带内,我们仍然需要一种方法找到附近跨边界点对,而不是枚举所有带内组合。网格(grid)使用边长 r / sqrt(2) 的单元格(cell),并围绕每个左侧当前格子检查固定的局部窗口。

Packing 不变量递归之后,任一半边内部都不能再藏着小于 r 的点对。
类别状态
左半边没有同侧点对 < r
右半边没有同侧点对 < r
局部窗口相关候选数量为常数

没有同侧点对比 r 更近

这就是为什么每个局部网格窗口只有常数个相关候选。

网格组织带内点使用边长 r / sqrt(2) 的格子,带内点保留左右标签,只发出左右跨边界点对。
x=5AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

格子边长 = 0.92

网格是查找结构,不是距离测试本身。

为什么 5 x 5 窗口足够abs(dx) 或 abs(dy) 至少为 3 的格子已经太远,不可能优于 r。
AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

dx、dy 都在 [-2,2]

这个窗口偏保守;最终仍由欧氏距离检查决定。

互动可视化

x=5AA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

阶段

  1. 朴素
  2. 分割
  3. 递归
  4. 带状区域
  5. 网格
  6. 检查
  7. 完成

朴素方法会比较每一对点。12 个点就需要 66 次检查,才能确定答案。

r:1.30
格子边长:0.92
已检查:1
当前点对:A-B
最佳点对:
类别

形式化版本

给定平面点集 P,按 x 坐标把它分成 PLPR。递归计算两边的最近距离 rLeftrRight,并令 r = min(rLeft, rRight)

唯一还没解决的是跨边界候选点对 (p, q)p 在一边,q 在另一边,并且 dist(p, q) < r

每个点对都有类别左左、右右点对已由递归解决;只有跨边界点对进入合并。
类别状态
左-左由左侧递归调用解决
右-右由右侧递归调用解决
跨边界只在合并阶段检查

同侧已解决;跨边界待解决

合并不是忽略同侧点对,而是在复用递归答案。

本页使用 dist(P, Q) = sqrt((Px - Qx)^2 + (Py - Qy)^2)。网格会保存带内点以及它们的左右标签,但只发出左-右跨边界候选点对。实现中使用稳定的左侧当前点顺序、规范化点对顺序,并在计数前去重。

F 的当前窗口F 从局部窗口发出 F-G 和 F-H;F-G 胜出。
x=5F-GAA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

先 F-G,再 F-H

只有左侧当前点发出候选,避免重复计数。

F 的候选点对:F-G、F-H。F-G 距离 0.71。

实现草图

function closestPair(points: Point[]): Distance {
  const unique = rejectDuplicateCoordinates(points);
  const byX = sortByX(unique);
  return solve(byX);
}

function solve(pointsByX: Point[]): Distance {
  if (pointsByX.length <= 3) return bruteForce(pointsByX);

  const { leftByX, rightByX, splitX } = splitSortedByX(pointsByX);
  const rLeft = solve(leftByX);
  const rRight = solve(rightByX);
  const r = Math.min(rLeft, rRight);

  return Math.min(r, closestCrossPair(pointsByX, splitX, r));
}

closestCrossPair 会为阈值带构建哈希网格。它使用半开区间格子,检查保守的 dx,dy in [-2,2] 格子窗口,并且在接受任何候选之前仍然计算真实欧氏距离。

代码对应追踪状态实现草图应当对应可见的追踪状态。
步骤追踪状态
sortByX(points)split-created
n <= 3 bruteForcerecursion-scaffold
r = min(rLeft, rRight)threshold-chosen
过滤阈值带threshold-band-only
检查网格窗口active-window-f

排序一次 -> 递归 -> 合并

保留排序顺序,小叶子用暴力法,然后用带状区域和网格合并。

正确性直觉

每个点对要么同侧,要么跨边界。同侧点对已经由递归处理。阈值带外的跨边界点对不可能优于 r。局部网格窗口外的跨边界点对也不可能优于 r。剩下被检查的点对,仍然需要真实距离比较。

跳过点对有原因每个点对要么已解决、在带外、在窗口外、检查后失败,或检查后胜出。
x=5F-GAA (0.8, 5.2), cell 0,5BB (1.6, 1.2), cell 1,1CC (2.4, 3.7), cell 2,4DD (3.6, 6), cell 3,6EE (4.2, 2.2), cell 4,2FF (4.6, 4.1), cell 5,4GG (5.3, 4), cell 5,4HH (5.8, 2.8), cell 6,3II (6.7, 5.9), cell 7,6JJ (7.5, 1.5), cell 8,1KK (8.2, 4.6), cell 8,5LL (9.1, 2.3), cell 9,2

F-G 属于 checked-wins

这些类别在追踪中必须互斥。

复杂度

先按 x 坐标排序一次,并在递归中传递有序列表,因此每一层只有期望线性的合并工作。哈希网格让非空格子查询达到期望 O(1)

T(n) <= 2T(n / 2) + O(n)

因此总期望时间复杂度是 O(n log n)

每层线性合并只排序一次,传递顺序,每层递归期望花 O(n) 工作。
层级工作量
0n
1n
2n
log nn

T(n) = 2T(n/2) + O(n)

因此得到期望 O(n log n),不是 O(n log^2 n)。

常见混淆

网格本身不是完整算法。它只是在递归已经给出距离阈值 r 之后,让合并步骤变便宜。

正好落在正向网格边界上的点,会进入右侧或上方格子。距离正好等于 r 的并列点对不会替换当前最佳。重复坐标会在构建网格之前直接返回距离 0

边界格子是左闭右开正向网格边界上的点进入右侧或上方格子。
输入期望结果
Q(0.999, 0.5)0,0
P(1, 0.5)1,0
R(2, 1)2,1

floor((coord - origin) / cellSize)

Q 在 0,0;P 在 1,0;R 在 2,1。

并列距离不会替换 r合并阶段寻找严格小于 r 的点对。
输入期望结果
dist(R,S) = r保留当前最佳

严格 < r

距离正好等于 r 的跨边界点对属于 checked-loses。

重复点在网格前返回重复坐标的距离为 0,因此不应根据 r = 0 构造格子边长。
输入期望结果
P(2,3) Q(2,3) R(5,4)P-Q = 0

重复点 -> 距离 0

P-Q 会以距离 0 立即返回。

图谱连接

最近点对分治和 Graham 扫描都是几何算法。最近点对依赖递归和局部跨边界合并;Graham 扫描依赖极角排序和栈不变量。

预测练习

  1. 在演示中,为什么同侧点对不需要在合并阶段重新检查?
  2. 经过阈值带过滤后、进入网格窗口规则前,还剩哪些点对?
  3. 为什么网格仍然要计算欧氏距离,而不是直接接受附近格子里的每个点对?

图谱连接 : 最近点对分治