Implementation of closest-points D&C algorithm

Implementation of closest-points D&C algorithm#

First, we generate the points.

import numpy as np
import plotly.graph_objects as go
import plotly.io as pio
pio.renderers.default = 'notebook'

def generate_2d_points(N):
    return 200*np.random.rand(N,2)-100

def plot_2d_points(points):
    fig = go.Figure(data=go.Scatter(
        x=points[:, 0],
        y=points[:, 1],
        mode='markers',
        marker=dict(
            size=10,
            opacity=0.7
        )
    ))

    fig.update_layout(width=600, height=600)
    return fig
N = 50
points = generate_2d_points(N)
fig = plot_2d_points(points)
fig.show()

Brute Force Implementation#

The brute force implementation has no surprises. It returns a tuple of the two points, and the distance between them.

def brute_force(points):
    bestpair = (points[0], points[1], np.linalg.norm(points[0]-points[1]))
    N, _ = points.shape

    for i in range(N-1):
        for j in range(i+1,N):
            dist = np.linalg.norm( points[i,:]-points[j,:] )
            if dist<bestpair[2]:
                bestpair = (points[i], points[j], dist.item())
    return bestpair
first, second, distance = brute_force(points)
print(first,second,distance)
[ 43.40312713 -22.57030696] [ 41.21756384 -26.76626592] 4.731042007145598
def highlight_closest(fig, point1, point2):
    fig.add_trace(go.Scatter(
        x=[point1[0], point2[0]],
        y=[point1[1], point2[1]],
        mode='markers',
        marker=dict(
            color='red',
            size=12
        )
    ))
    fig.update_layout(showlegend=False)
    return fig
fig = highlight_closest(fig, first, second )
fig.show()

Divide and Conquer Implementation#

Again, the algorithm is:

  • Presort the points by the x-axis.

  • Recursively divide by the median x-value. When the number of points gets small, solve using the brute force approach.

  • When combining, the solution is either the solution from the left half, the solution from the right half, or a point pair that straddles the boundary.

    • To find pairs that straddle the boundary, we first select only the points closer to the boundary than the closest pair from the left or right halves

    • We sort these points in that strip by the y-value

    • Each point only needs to be compared to a small, constant number of nearby points

def strip_closest(strip_points, min_dist_so_far):
    min_dist = min_dist_so_far
    closest_pair = (None, None)
    
    for i in range(strip_points.shape[0]):
        for j in range(i + 1, min(i + 8, strip_points.shape[0])):
            p1 = strip_points[i]
            p2 = strip_points[j]
            dist = np.linalg.norm(p1-p2)
            
            if dist < min_dist:
                min_dist = dist
                closest_pair = (p1, p2)
                
    return closest_pair[0], closest_pair[1], min_dist

def find_closest_pair_dc(points_sorted_x):
    N = points_sorted_x.shape[0]
    
    # Base Case: If N <= 3, use brute force
    if N <= 3:
        return brute_force(points_sorted_x)

    # Divide Step
    mid = N // 2
    mid_x = points_sorted_x[mid, 0]
    
    p1_l, p2_l, dist_l = find_closest_pair_dc(points_sorted_x[:mid])
    p1_r, p2_r, dist_r = find_closest_pair_dc(points_sorted_x[mid:])

    if dist_l < dist_r:
        min_dist, closest_p1, closest_p2 = dist_l, p1_l, p2_l
    else:
        min_dist, closest_p1, closest_p2 = dist_r, p1_r, p2_r
    
    strip = points_sorted_x[np.abs(points_sorted_x[:, 0] - mid_x) < min_dist]
    
    strip_sorted_y = strip[strip[:, 1].argsort()]

    p1_s, p2_s, dist_strip = strip_closest(strip_sorted_y, min_dist)
    
    if dist_strip < min_dist:
        return p1_s, p2_s, dist_strip
    else:
        return closest_p1, closest_p2, min_dist

def closest_pair_starter(points):
    if points.shape[0] < 2:
        return np.inf, None, None
    
    points_sorted_x = points[points[:, 0].argsort()]
    return find_closest_pair_dc(points_sorted_x)
first, second, dist = closest_pair_starter(points)
print(first, second, dist)
[ 41.21756384 -26.76626592] [ 43.40312713 -22.57030696] 4.731042007145598
fig = plot_2d_points(points)
fig = highlight_closest(fig, first, second)
fig.show()

Comparison#

With only 5000 points, the O(n^2) and O(n log n) algorithms have a significant real difference in speed.

import time

N = 5000
points = generate_2d_points(N)

start = time.time()
first, second, distance = brute_force(points)
end = time.time()
print(f'Best points are {distance} apart, and brute force took {end-start} seconds.')

start = time.time()
first, second, distance = closest_pair_starter(points)
end = time.time()
print(f'Best points are {distance} apart, and D&C took {end-start} seconds.')
Best points are 0.0196075187992648 apart, and brute force took 16.488074779510498 seconds.
Best points are 0.0196075187992648 apart, and D&C took 0.11477851867675781 seconds.