-
Notifications
You must be signed in to change notification settings - Fork 20k
Randomized closest 2 points algorithm #6251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
82fb1f5
f1391bd
63ddb36
21fed7d
b4c88a7
d7949d6
a7c2163
852c2e7
f854cf7
b18bce9
d1f7ec5
f49710a
94c5ad4
b5c62ab
e89e6c4
c93383b
0e32962
cac9706
e05cfdd
c386ec9
bb66f17
92ff833
20d0d31
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,102 +1,106 @@ | ||
package com.thealgorithms.randomized; | ||
import java.math.BigDecimal; | ||
import java.math.RoundingMode; | ||
import java.util.ArrayList; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.HashMap; | ||
import java.util.HashSet; | ||
import java.util.List; | ||
import java.util.Map; | ||
import java.util.Random; | ||
import java.util.Set; | ||
|
||
// As required by Repository, new algorithms have URL in comments with explanation | ||
// https://www.geeksforgeeks.org/closest-pair-of-points-using-divide-and-conquer-algorithm | ||
// Given 2 or more points on a 2-dimensional plane, find the closest 2 points in Euclidean distance | ||
// This class uses the divide and conquer technique with recursion | ||
class Point { | ||
double x, y; | ||
|
||
final class Point implements Comparable<Point> { | ||
double x; | ||
double y; | ||
|
||
// Constructor to initialize a point with x and y coordinates | ||
Point(double x, double y) { | ||
public Point(double x, double y) { | ||
this.x = x; | ||
this.y = y; | ||
} | ||
|
||
public int compareTo(Point other) { | ||
return Double.compare(this.x, other.x); | ||
} | ||
|
||
static double distance(Point p1, Point p2) { | ||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); | ||
@Override | ||
public String toString() { | ||
return "(" + x + ", " + y + ")"; | ||
} | ||
} | ||
|
||
public final class ClosestPair { | ||
// Private constructor to prevent instantiation | ||
private ClosestPair() { | ||
throw new AssertionError("Utility class should not be instantiated."); | ||
} | ||
|
||
public static double closest(List<Point> points) { | ||
if (points == null || points.isEmpty()) { | ||
throw new IllegalArgumentException("There are no pairs to compare."); | ||
} | ||
|
||
if (points.size() == 1) { | ||
throw new IllegalArgumentException("There is only one pair."); | ||
} | ||
|
||
Collections.sort(points); | ||
double result = closestRecursiveHelper(points, 0, points.size() - 1); | ||
public class ClosestPair { | ||
private static final double INFINITY = Double.MAX_VALUE; | ||
|
||
// Return distance of closest pair rounded to 2 decimal places | ||
return new BigDecimal(String.valueOf(result)).setScale(2, RoundingMode.HALF_UP).doubleValue(); | ||
public static double euclideanDistance(Point p1, Point p2) { | ||
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2)); | ||
} | ||
|
||
private static double closestRecursiveHelper(List<Point> points, int left, int right) { | ||
// Base Case occurs with 3 or fewer points | ||
if (right - left <= 2) { | ||
return baseCase(points, left, right); | ||
/** | ||
* Algorithm Proof https://www.cs.toronto.edu/~anikolov/CSC473W20/kt-rabin.pdf | ||
* Additional information: https://en.wikipedia.org/wiki/Closest_pair_of_points_problem | ||
* This class uses Rabin's randomized approach to find the closest pair of points. | ||
* Rabin's approach randomly selects a sample of points to estimate an initial closest distance | ||
* (delta), then uses a grid for "probabilistic refinement". Finally, it updates the closest pair | ||
* with the closest distance. | ||
*/ | ||
|
||
public static Object[] rabinRandomizedClosestPair(List<Point> points) { | ||
// Error handling, must have at least 2 points | ||
if (points == null || points.size() < 2) { | ||
return new Object[] {null, null, INFINITY}; | ||
} | ||
|
||
// Divide and conquer | ||
int mid = (left + right) / 2; | ||
double midX = points.get(mid).x; | ||
|
||
double leftDist = closestRecursiveHelper(points, left, mid); | ||
double rightDist = closestRecursiveHelper(points, mid + 1, right); | ||
Collections.shuffle(points, new Random()); // shuffle for required randomness | ||
|
||
double minDist = Math.min(leftDist, rightDist); | ||
double delta = INFINITY; // initialize distance | ||
Point closestA = null; | ||
Point closestB = null; | ||
|
||
return checkBoundary(points, left, right, midX, minDist); | ||
} | ||
// without exceeding number of points, work with some sample | ||
int sampleSize = Math.min(7, points.size()); | ||
|
||
private static double baseCase(List<Point> points, int left, int right) { | ||
// Sub-problems fitting the base case can use brute force | ||
double minDist = Double.MAX_VALUE; | ||
for (int i = left; i <= right; i++) { | ||
for (int j = i + 1; j <= right; j++) { | ||
minDist = Math.min(minDist, Point.distance(points.get(i), points.get(j))); | ||
} | ||
Random random = new Random(); // select randomly | ||
Set<Point> sampleSet = new HashSet<>(); // ensure unique pairs | ||
while (sampleSet.size() < sampleSize) { | ||
sampleSet.add(points.get(random.nextInt(points.size()))); | ||
} | ||
return minDist; | ||
} | ||
|
||
private static double checkBoundary(List<Point> points, int left, int right, double midX, double minDist) { | ||
// Consider a boundary by the dividing line | ||
List<Point> boundary = new ArrayList<>(); | ||
for (int i = left; i <= right; i++) { | ||
if (Math.abs(points.get(i).x - midX) < minDist) { | ||
boundary.add(points.get(i)); | ||
List<Point> sample = new ArrayList<>(sampleSet); | ||
|
||
// initially the closest points are found via brute force | ||
for (int i = 0; i < sample.size(); i++) { | ||
for (int j = i + 1; j < sample.size(); j++) { | ||
double dist = euclideanDistance(sample.get(i), sample.get(j)); | ||
if (dist < delta) { | ||
closestA = sample.get(i); | ||
closestB = sample.get(j); | ||
delta = dist; // update distance | ||
} | ||
} | ||
} | ||
|
||
// sort by y coordinate within the boundary and check for closer points | ||
boundary.sort(Comparator.comparingDouble(p -> p.y)); | ||
for (int i = 0; i < boundary.size(); i++) { | ||
for (int j = i + 1; j < boundary.size() && (boundary.get(j).y - boundary.get(i).y) < minDist; j++) { | ||
minDist = Math.min(minDist, Point.distance(boundary.get(i), boundary.get(j))); | ||
// Create a grid, We will use "Probabilistic Filtering" by only checking | ||
// neighboring grids to prevent bruteforce checking outside initialization | ||
Map<String, Point> grid = new HashMap<>(); | ||
|
||
// coordinates computed based on delta, estimated closest distance | ||
for (Point p : points) { | ||
int gridX = (int) (p.x / delta); | ||
int gridY = (int) (p.y / delta); | ||
String key = gridX + "," + gridY; // string for indexing | ||
|
||
// check neighboring cells | ||
for (int dX = -1; dX <= 1; dX++) { | ||
for (int dY = -1; dY <= 1; dY++) { | ||
String neighborKey = (gridX + dX) + "," + (gridY + dY); | ||
Point neighborValue = grid.get(neighborKey); | ||
|
||
// update points only if valid neighbor | ||
if (neighborValue != null && p != neighborValue) { | ||
double dist = euclideanDistance(p, neighborValue); | ||
if (dist < delta) { | ||
closestA = p; | ||
closestB = neighborValue; | ||
delta = dist; | ||
} | ||
} | ||
} | ||
} | ||
grid.put(key, p); | ||
} | ||
return minDist; | ||
return new Object[] {closestA, closestB, delta}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,43 +1,57 @@ | ||
package com.thealgorithms.randomized; | ||
bri-harris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertThrows; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Random; | ||
import org.junit.jupiter.api.Test; | ||
|
||
public class ClosestPairTest { | ||
class ClosestPairTest { | ||
|
||
// Tests sorting of an array with multiple elements, including duplicates. | ||
@Test | ||
public void testMultiplePairs() { | ||
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)); | ||
double expected = 1.41; | ||
assertEquals(expected, ClosestPair.closest(points)); | ||
void testStandardCaseClosestPair() { | ||
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)); | ||
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points); | ||
assertNotEquals(closestPair[0], closestPair[1], "Points are distinct"); | ||
assertTrue((double) closestPair[2] > 0, "Distance must be positive"); | ||
} | ||
|
||
// Test if there are no pairs. | ||
@Test | ||
public void testNoPoints() { | ||
List<Point> points = new ArrayList<>(); | ||
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); }); | ||
assertEquals("There are no pairs to compare.", exception.getMessage()); | ||
void testTwoDistinctPoints() { | ||
List<Point> points = Arrays.asList(new Point(1, 2), new Point(2, 3)); | ||
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points); | ||
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)))); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure this test is correct? Because the .equals method is not overwritten in the Point class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could you please refine and improve the test a bit? The likely cause of the NullPointer warning is that the analysis tool doesn't recognize assertNotNull() as a safe null check—assigning the value to a variable before asserting might help. Thats probably why infer is failing |
||
assertEquals(closestPair[2], ClosestPair.euclideanDistance(points.get(0), points.get(1))); | ||
} | ||
|
||
// Test if there is one point, no pairs. | ||
@Test | ||
public void testOnePoint() { | ||
List<Point> points = Arrays.asList(new Point(1, 2)); | ||
Exception exception = assertThrows(IllegalArgumentException.class, () -> { ClosestPair.closest(points); }); | ||
assertEquals("There is only one pair.", exception.getMessage()); | ||
void testIdenticalPointsPairWithDistanceZero() { | ||
List<Point> points = Arrays.asList(new Point(1.0, 2.0), new Point(1.0, 2.0), new Point(1.0, 1.0)); | ||
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points); | ||
assertTrue((closestPair[0].equals(points.get(0)) && closestPair[1].equals(points.get(1)))); | ||
assertEquals(0, (double) closestPair[2], "Distance is zero"); | ||
} | ||
|
||
@Test | ||
void testLargeDatasetRandomPoints() { | ||
List<Point> points = new ArrayList<>(); | ||
Random random = new Random(); | ||
for (int i = 0; i < 1000; i++) { | ||
points.add(new Point(random.nextDouble() * 100, random.nextDouble() * 100)); | ||
} | ||
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points); | ||
assertNotNull(closestPair[0]); | ||
assertNotNull(closestPair[1]); | ||
assertTrue((double) closestPair[2] > 0, "Distance must be positive"); | ||
} | ||
|
||
// Test if there is a duplicate points as a pair | ||
@Test | ||
public void testPoints() { | ||
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)); | ||
double expected = 0.00; | ||
assertEquals(expected, ClosestPair.closest(points)); | ||
void testSinglePointShouldReturnNoPair() { | ||
List<Point> points = Arrays.asList(new Point(5.0, 5.0)); | ||
Object[] closestPair = ClosestPair.rabinRandomizedClosestPair(points); | ||
assertNull(closestPair[0]); | ||
assertNull(closestPair[1]); | ||
} | ||
} | ||
bri-harris marked this conversation as resolved.
Show resolved
Hide resolved
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think currently, only one point is stored per grid cell, overwriting any existing points. This can cause missed closest pairs when multiple points fall into the same cell. It is recommended to store a list of points per cell to ensure correctness...