Skip to content

Commit bbd5f1a

Browse files
committed
Use a true randomized algorithm solution, Rabin's randomized
1 parent 3150056 commit bbd5f1a

File tree

2 files changed

+113
-95
lines changed

2 files changed

+113
-95
lines changed
Lines changed: 77 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,106 @@
11
package com.thealgorithms.randomized;
2-
import java.math.BigDecimal;
3-
import java.math.RoundingMode;
42
import java.util.ArrayList;
53
import java.util.Collections;
6-
import java.util.Comparator;
4+
import java.util.HashMap;
5+
import java.util.HashSet;
76
import java.util.List;
7+
import java.util.Map;
8+
import java.util.Random;
9+
import java.util.Set;
810

9-
// As required by Repository, new algorithms have URL in comments with explanation
10-
// https://www.geeksforgeeks.org/closest-pair-of-points-using-divide-and-conquer-algorithm
11-
// Given 2 or more points on a 2-dimensional plane, find the closest 2 points in Euclidean distance
12-
// This class uses the divide and conquer technique with recursion
11+
class Point {
12+
double x, y;
1313

14-
final class Point implements Comparable<Point> {
15-
double x;
16-
double y;
17-
18-
// Constructor to initialize a point with x and y coordinates
19-
Point(double x, double y) {
14+
public Point(double x, double y) {
2015
this.x = x;
2116
this.y = y;
2217
}
2318

24-
public int compareTo(Point other) {
25-
return Double.compare(this.x, other.x);
26-
}
27-
28-
static double distance(Point p1, Point p2) {
29-
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
19+
@Override
20+
public String toString() {
21+
return "(" + x + ", " + y + ")";
3022
}
3123
}
3224

33-
public final class ClosestPair {
34-
// Private constructor to prevent instantiation
35-
private ClosestPair() {
36-
throw new AssertionError("Utility class should not be instantiated.");
37-
}
38-
39-
public static double closest(List<Point> points) {
40-
if (points == null || points.isEmpty()) {
41-
throw new IllegalArgumentException("There are no pairs to compare.");
42-
}
43-
44-
if (points.size() == 1) {
45-
throw new IllegalArgumentException("There is only one pair.");
46-
}
47-
48-
Collections.sort(points);
49-
double result = closestRecursiveHelper(points, 0, points.size() - 1);
25+
public class ClosestPair {
26+
private static final double INFINITY = Double.MAX_VALUE;
5027

51-
// Return distance of closest pair rounded to 2 decimal places
52-
return new BigDecimal(String.valueOf(result)).setScale(2, RoundingMode.HALF_UP).doubleValue();
28+
public static double euclideanDistance(Point p1, Point p2) {
29+
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
5330
}
5431

55-
private static double closestRecursiveHelper(List<Point> points, int left, int right) {
56-
// Base Case occurs with 3 or fewer points
57-
if (right - left <= 2) {
58-
return baseCase(points, left, right);
32+
/**
33+
* Algorithm Proof https://www.cs.toronto.edu/~anikolov/CSC473W20/kt-rabin.pdf
34+
* Additional information: https://en.wikipedia.org/wiki/Closest_pair_of_points_problem
35+
* This class uses Rabin's randomized approach to find the closest pair of points.
36+
* Rabin's approach randomly selects a sample of points to estimate an initial closest distance
37+
* (delta), then uses a grid for "probabilistic refinement". Finally, it updates the closest pair
38+
* with the closest distance.
39+
*/
40+
41+
public static Object[] rabinRandomizedClosestPair(List<Point> points) {
42+
// Error handling, must have at least 2 points
43+
if (points == null || points.size() < 2) {
44+
return new Object[] {null, null, INFINITY};
5945
}
6046

61-
// Divide and conquer
62-
int mid = (left + right) / 2;
63-
double midX = points.get(mid).x;
64-
65-
double leftDist = closestRecursiveHelper(points, left, mid);
66-
double rightDist = closestRecursiveHelper(points, mid + 1, right);
47+
Collections.shuffle(points, new Random()); // shuffle for required randomness
6748

68-
double minDist = Math.min(leftDist, rightDist);
49+
double delta = INFINITY; // initialize distance
50+
Point closestA = null;
51+
Point closestB = null;
6952

70-
return checkBoundary(points, left, right, midX, minDist);
71-
}
53+
// without exceeding number of points, work with some sample
54+
int sampleSize = Math.min(7, points.size());
7255

73-
private static double baseCase(List<Point> points, int left, int right) {
74-
// Sub-problems fitting the base case can use brute force
75-
double minDist = Double.MAX_VALUE;
76-
for (int i = left; i <= right; i++) {
77-
for (int j = i + 1; j <= right; j++) {
78-
minDist = Math.min(minDist, Point.distance(points.get(i), points.get(j)));
79-
}
56+
Random random = new Random(); // select randomly
57+
Set<Point> sampleSet = new HashSet<>(); // ensure unique pairs
58+
while (sampleSet.size() < sampleSize) {
59+
sampleSet.add(points.get(random.nextInt(points.size())));
8060
}
81-
return minDist;
82-
}
83-
84-
private static double checkBoundary(List<Point> points, int left, int right, double midX, double minDist) {
85-
// Consider a boundary by the dividing line
86-
List<Point> boundary = new ArrayList<>();
87-
for (int i = left; i <= right; i++) {
88-
if (Math.abs(points.get(i).x - midX) < minDist) {
89-
boundary.add(points.get(i));
61+
List<Point> sample = new ArrayList<>(sampleSet);
62+
63+
// initially the closest points are found via brute force
64+
for (int i = 0; i < sample.size(); i++) {
65+
for (int j = i + 1; j < sample.size(); j++) {
66+
double dist = euclideanDistance(sample.get(i), sample.get(j));
67+
if (dist < delta) {
68+
closestA = sample.get(i);
69+
closestB = sample.get(j);
70+
delta = dist; // update distance
71+
}
9072
}
9173
}
9274

93-
// sort by y coordinate within the boundary and check for closer points
94-
boundary.sort(Comparator.comparingDouble(p -> p.y));
95-
for (int i = 0; i < boundary.size(); i++) {
96-
for (int j = i + 1; j < boundary.size() && (boundary.get(j).y - boundary.get(i).y) < minDist; j++) {
97-
minDist = Math.min(minDist, Point.distance(boundary.get(i), boundary.get(j)));
75+
// Create a grid, We will use "Probabilistic Filtering" by only checking
76+
// neighboring grids to prevent bruteforce checking outside initialization
77+
Map<String, Point> grid = new HashMap<>();
78+
79+
// coordinates computed based on delta, estimated closest distance
80+
for (Point p : points) {
81+
int gridX = (int) (p.x / delta);
82+
int gridY = (int) (p.y / delta);
83+
String key = gridX + "," + gridY; // string for indexing
84+
85+
// check neighboring cells
86+
for (int dX = -1; dX <= 1; dX++) {
87+
for (int dY = -1; dY <= 1; dY++) {
88+
String neighborKey = (gridX + dX) + "," + (gridY + dY);
89+
Point neighborValue = grid.get(neighborKey);
90+
91+
// update points only if valid neighbor
92+
if (neighborValue != null && p != neighborValue) {
93+
double dist = euclideanDistance(p, neighborValue);
94+
if (dist < delta) {
95+
closestA = p;
96+
closestB = neighborValue;
97+
delta = dist;
98+
}
99+
}
100+
}
98101
}
102+
grid.put(key, p);
99103
}
100-
return minDist;
104+
return new Object[] {closestA, closestB, delta};
101105
}
102106
}
Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,57 @@
11
package com.thealgorithms.randomized;
2+
import static org.junit.jupiter.api.Assertions.*;
23
import static org.junit.jupiter.api.Assertions.assertEquals;
3-
import static org.junit.jupiter.api.Assertions.assertThrows;
44

55
import java.util.ArrayList;
66
import java.util.Arrays;
77
import java.util.List;
8+
import java.util.Random;
89
import org.junit.jupiter.api.Test;
910

10-
public class ClosestPairTest {
11+
class ClosestPairTest {
1112

12-
// Tests sorting of an array with multiple elements, including duplicates.
1313
@Test
14-
public void testMultiplePairs() {
15-
List<Point> points = Arrays.asList(new Point(1, 2), new Point(3, 4), new Point(5, 1), new Point(7, 8), new Point(2, 3), new Point(6, 2));
16-
double expected = 1.41;
17-
assertEquals(expected, ClosestPair.closest(points));
14+
void testStandardCaseClosestPair() {
15+
List<Point> points = Arrays.asList(new Point(1, 4), new Point(2, 8), new Point(0, 1), new Point(4, 5), new Point(9, 4));
16+
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
17+
assertNotEquals(closestPair[0], closestPair[1], "Points are distinct");
18+
assertTrue((double) closestPair[2] > 0, "Distance must be positive");
1819
}
1920

20-
// Test if there are no pairs.
2121
@Test
22-
public void testNoPoints() {
23-
List<Point> points = new ArrayList<>();
24-
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); });
25-
assertEquals("There are no pairs to compare.", exception.getMessage());
22+
void testTwoDistinctPoints() {
23+
List<Point> points = Arrays.asList(new Point(1, 2), new Point(2, 3));
24+
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
25+
assertTrue((closestPair[0].equals(points.get(0)) && closestPair[1].equals(points.get(1))) || (closestPair[1].equals(points.get(0)) && closestPair[0].equals(points.get(1))));
26+
assertEquals(closestPair[2], ClosestPair.euclideanDistance(points.get(0), points.get(1)));
2627
}
2728

28-
// Test if there is one point, no pairs.
2929
@Test
30-
public void testOnePoint() {
31-
List<Point> points = Arrays.asList(new Point(1, 2));
32-
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); });
33-
assertEquals("There is only one pair.", exception.getMessage());
30+
void testIdenticalPointsPairWithDistanceZero() {
31+
List<Point> points = Arrays.asList(new Point(1.0, 2.0), new Point(1.0, 2.0), new Point(1.0, 1.0));
32+
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
33+
assertTrue((closestPair[0].equals(points.get(0)) && closestPair[1].equals(points.get(1))));
34+
assertEquals(0, (double) closestPair[2], "Distance is zero");
35+
}
36+
37+
@Test
38+
void testLargeDatasetRandomPoints() {
39+
List<Point> points = new ArrayList<>();
40+
Random random = new Random();
41+
for (int i = 0; i < 1000; i++) {
42+
points.add(new Point(random.nextDouble() * 100, random.nextDouble() * 100));
43+
}
44+
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
45+
assertNotNull(closestPair[0]);
46+
assertNotNull(closestPair[1]);
47+
assertTrue((double) closestPair[2] > 0, "Distance must be positive");
3448
}
3549

36-
// Test if there is a duplicate points as a pair
3750
@Test
38-
public void testPoints() {
39-
List<Point> points = Arrays.asList(new Point(1, 2), new Point(5, 1), new Point(5, 1), new Point(7, 8), new Point(2, 3), new Point(6, 2));
40-
double expected = 0.00;
41-
assertEquals(expected, ClosestPair.closest(points));
51+
void testSinglePointShouldReturnNoPair() {
52+
List<Point> points = Arrays.asList(new Point(5.0, 5.0));
53+
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points);
54+
assertNull(closestPair[0]);
55+
assertNull(closestPair[1]);
4256
}
4357
}

0 commit comments

Comments
 (0)