最近点对问题(Closest Pair of Points)

📌 定义

给定平面上的n个点,找出距离最近的两个点。这是计算几何中的经典问题,暴力解法需要O(n²)时间,而使用分治算法可以优化到O(n log n)。

示例:
点集: [(1,1), (3,3), (2,2), (5,5), (4,4)]

最近点对: (2,2) 和 (3,3)
距离: √[(3-2)² + (3-2)²] = √2 ≈ 1.414

核心思路

使用分治法:

  1. 分解:将点集按x坐标排序,从中间分成左右两部分
  2. 递归:分别找出左半部分和右半部分的最近点对
  3. 合并:检查跨越分割线的点对,更新最小距离

关键优化:合并时只需检查距离分割线距离小于δ的点,且这些点按y坐标排序后只需检查相邻的几个点。

点集按x坐标分割:
     |
  ●  |  ●
 ●   | ●
  ●  |   ●
     |  ●
  ●  | ●
     |
   左半  右半

最近点对可能在:
1. 完全在左半部分
2. 完全在右半部分
3. 跨越分割线

复杂度分析

方法时间复杂度空间复杂度说明
暴力枚举O(n²)O(1)检查所有点对
分治法O(n log n)O(n)预排序+分治

Go 代码

Go 实现

package main
 
import (
    "fmt"
    "math"
    "sort"
)
 
type Point struct {
    X, Y float64
}
 
func distance(p1, p2 Point) float64 {
    dx := p1.X - p2.X
    dy := p1.Y - p2.Y
    return math.Sqrt(dx*dx + dy*dy)
}
 
func closestPair(points []Point) (float64, Point, Point) {
    // 预处理:排序
    px := make([]Point, len(points))
    py := make([]Point, len(points))
    copy(px, points)
    copy(py, points)
 
    sort.Slice(px, func(i, j int) bool {
        return px[i].X < px[j].X
    })
 
    sort.Slice(py, func(i, j int) bool {
        return py[i].Y < py[j].Y
    })
 
    return closestPairRec(px, py)
}
 
func closestPairRec(px, py []Point) (float64, Point, Point) {
    n := len(px)
 
    // 基准情况
    if n <= 3 {
        return bruteForce(px)
    }
 
    mid := n / 2
    midPoint := px[mid]
 
    // 分割
    var pyl, pyr []Point
    for _, p := range py {
        if p.X <= midPoint.X {
            pyl = append(pyl, p)
        } else {
            pyr = append(pyr, p)
        }
    }
 
    // 递归
    dl, p1l, p2l := closestPairRec(px[:mid], pyl)
    dr, p1r, p2r := closestPairRec(px[mid:], pyr)
 
    var d float64
    var p1, p2 Point
 
    if dl < dr {
        d = dl
        p1, p2 = p1l, p2l
    } else {
        d = dr
        p1, p2 = p1r, p2r
    }
 
    // 检查strip
    var strip []Point
    for _, p := range py {
        if math.Abs(p.X-midPoint.X) < d {
            strip = append(strip, p)
        }
    }
 
    for i := 0; i < len(strip); i++ {
        j := i + 1
        for j < len(strip) && (strip[j].Y-strip[i].Y) < d {
            dist := distance(strip[i], strip[j])
            if dist < d {
                d = dist
                p1, p2 = strip[i], strip[j]
            }
            j++
        }
    }
 
    return d, p1, p2
}
 
func bruteForce(points []Point) (float64, Point, Point) {
    minDist := math.Inf(1)
    var p1, p2 Point
 
    for i := 0; i < len(points); i++ {
        for j := i + 1; j < len(points); j++ {
            d := distance(points[i], points[j])
            if d < minDist {
                minDist = d
                p1, p2 = points[i], points[j]
            }
        }
    }
 
    return minDist, p1, p2
}
 
func main() {
    points := []Point{
        {1, 1}, {3, 3}, {2, 2}, {5, 5}, {4, 4}, {7, 1}, {8, 9},
    }
 
    minDist, p1, p2 := closestPair(points)
 
    fmt.Printf("最近点对: (%.1f,%.1f) 和 (%.1f,%.1f)\n", p1.X, p1.Y, p2.X, p2.Y)
    fmt.Printf("最小距离: %.4f\n", minDist)
}

思路展开

为什么只需检查7个点?

关键观察:
在strip中,对于任意点p,在距离d内的矩形区域中:
- 矩形大小: 2d × d
- 左半部分 d × d 最多4个点(否则距离<d)
- 右半部分 d × d 最多4个点
- 总共最多8个点(包括p自己)

因此只需检查后面最多7个点!

复杂度推导

T(n) = 求解n个点的最近点对时间

T(n) = 2T(n/2) + O(n)
       ↑        ↑
     递归求解  合并

其中合并步骤:
- 构建strip: O(n)
- 检查strip: O(n) (每个点最多检查7个)

根据主定理:
a = 2, b = 2, f(n) = O(n)
log_b(a) = log_2(2) = 1

因为 f(n) = Θ(n^1)
所以 T(n) = O(n log n)

经典题目

基础应用

  • 最近点对距离
  • 所有最近点对
  • K最近点对

扩展问题

  • 三维空间最近点对
  • 曼哈顿距离最近点对
  • 动态最近点对(支持插入删除)

⚖️ 优缺点

优点

  • ✅ 高效:O(n log n)
  • ✅ 最优:已被证明是最优算法
  • ✅ 可扩展:可推广到高维空间

缺点

  • ❌ 实现复杂:需要仔细处理边界情况
  • ❌ 空间开销:需要额外的排序数组

🎨 应用场景

  1. 计算几何:最近邻查询
  2. 聚类分析:数据点分组
  3. 碰撞检测:游戏和物理模拟
  4. 地理信息系统:找最近的设施
  5. 模式识别:特征匹配

💡 变体问题

相关主题


返回:分治算法 | 算法学习导航