From bda7b7b5d00d29b238a3c3a45c3b0f2feaccf0f8 Mon Sep 17 00:00:00 2001 From: Simon Lammer Date: Tue, 6 Oct 2020 10:55:56 +0200 Subject: [PATCH 1/5] Implement the melkman anlgorithm for computing convex hulls --- divide_and_conquer/convex_hull.py | 72 ++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index cf2c7f835798..4fa8f306853f 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -411,6 +411,72 @@ def _construct_hull(points, left, right, convex_set): _construct_hull(candidate_points, extreme_point, right, convex_set) +def convex_hull_melkman(points): + """ + Constructs the convex hull of a set of 2D points using the melkman algorithm. + The algorithm works by iteratively inserting points of a simple polygonal chain + (meaning that no line segments between two consecutive points cross each other). + Sorting the points yields such a polygonal chain. + + Runtime: O(n log n) - O(n) if points are already sorted in the input + + Parameters + --------- + points: array-like of object of Points, lists or tuples. + The set of 2d points for which the convex-hull is needed + + Returns + ------ + convex_set: list, the convex-hull of points sorted in non-decreasing order. + + See Also + -------- + + Examples + --------- + >>> convex_hull_melkman([[0, 0], [1, 0], [10, 1]]) + [(0.0, 0.0), (1.0, 0.0), (10.0, 1.0)] + >>> convex_hull_melkman([[0, 0], [1, 0], [10, 0]]) + [(0.0, 0.0), (10.0, 0.0)] + >>> convex_hull_melkman([[-1, 1],[-1, -1], [0, 0], [0.5, 0.5], [1, -1], [1, 1], + ... [-0.75, 1]]) + [(-1.0, -1.0), (-1.0, 1.0), (1.0, -1.0), (1.0, 1.0)] + >>> convex_hull_melkman([(0, 3), (2, 2), (1, 1), (2, 1), (3, 0), (0, 0), (3, 3), + ... (2, -1), (2, -4), (1, -3)]) + [(0.0, 0.0), (0.0, 3.0), (1.0, -3.0), (2.0, -4.0), (3.0, 0.0), (3.0, 3.0)] + """ + points = sorted(_validate_input(points)) + n = len(points) + + convex_hull = points[:2] + for i in range(2, n): + det = _det(convex_hull[1], convex_hull[0], points[i]) + if det > 0: + convex_hull.insert(0, points[i]) + break + elif det < 0: + convex_hull.append(points[i]) + break + else: + convex_hull[1] = points[i] + i += 1 + + for i in range(i, n): + if (_det(convex_hull[0], convex_hull[-1], points[i]) > 0 + and _det(convex_hull[-1], convex_hull[0], points[1]) < 0): + # The point lies within the convex hull + continue + + convex_hull.insert(0, points[i]) + convex_hull.append(points[i]) + while _det(convex_hull[0], convex_hull[1], convex_hull[2]) >= 0: + del convex_hull[1] + while _det(convex_hull[-1], convex_hull[-2], convex_hull[-3]) <= 0: + del convex_hull[-2] + + # `convex_hull` is contains the convex hull in circular order + return sorted(convex_hull[1:] if len(convex_hull) > 3 else convex_hull) + def main(): points = [ (0, 3), @@ -426,10 +492,14 @@ def main(): ] # the convex set of points is # [(0, 0), (0, 3), (1, -3), (2, -4), (3, 0), (3, 3)] - results_recursive = convex_hull_recursive(points) results_bf = convex_hull_bf(points) + + results_recursive = convex_hull_recursive(points) assert results_bf == results_recursive + results_melkman = convex_hull_melkman(points) + assert results_bf == results_melkman + print(results_bf) From 6492d4b328b6d675cd8f7a1a26904c47d901015b Mon Sep 17 00:00:00 2001 From: Simon Lammer Date: Tue, 6 Oct 2020 11:03:22 +0200 Subject: [PATCH 2/5] Link melkman algorithm description --- divide_and_conquer/convex_hull.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 4fa8f306853f..6d1ecb93f587 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -418,6 +418,8 @@ def convex_hull_melkman(points): (meaning that no line segments between two consecutive points cross each other). Sorting the points yields such a polygonal chain. + For a detailed description, see http://cgm.cs.mcgill.ca/~athens/cs601/Melkman.html + Runtime: O(n log n) - O(n) if points are already sorted in the input Parameters From 7e0fe0e5ad42af314c06b45015e51e95ada0fa11 Mon Sep 17 00:00:00 2001 From: Simon Lammer Date: Tue, 6 Oct 2020 11:07:52 +0200 Subject: [PATCH 3/5] Format melkman algorithm code --- divide_and_conquer/convex_hull.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 6d1ecb93f587..40b3d792a5e6 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -464,8 +464,10 @@ def convex_hull_melkman(points): i += 1 for i in range(i, n): - if (_det(convex_hull[0], convex_hull[-1], points[i]) > 0 - and _det(convex_hull[-1], convex_hull[0], points[1]) < 0): + if ( + _det(convex_hull[0], convex_hull[-1], points[i]) > 0 + and _det(convex_hull[-1], convex_hull[0], points[1]) < 0 + ): # The point lies within the convex hull continue @@ -479,6 +481,7 @@ def convex_hull_melkman(points): # `convex_hull` is contains the convex hull in circular order return sorted(convex_hull[1:] if len(convex_hull) > 3 else convex_hull) + def main(): points = [ (0, 3), From 3a0316fc421ed432d1e33af0a8cbb44f09801113 Mon Sep 17 00:00:00 2001 From: Simon Lammer Date: Thu, 8 Oct 2020 20:14:57 +0200 Subject: [PATCH 4/5] Add type hints to functions --- divide_and_conquer/convex_hull.py | 54 +++++++++++-------------------- 1 file changed, 19 insertions(+), 35 deletions(-) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 40b3d792a5e6..459b83aa6487 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -13,6 +13,7 @@ """ +from typing import Iterable, List, Set, Union class Point: """ @@ -81,7 +82,7 @@ def __hash__(self): return hash(self.x) -def _construct_points(list_of_tuples): +def _construct_points(list_of_tuples: Union[List[Point], List[List[float]], Iterable[List[float]]]) -> List[Point]: """ constructs a list of points from an array-like object of numbers @@ -113,17 +114,20 @@ def _construct_points(list_of_tuples): points = [] if list_of_tuples: for p in list_of_tuples: - try: - points.append(Point(p[0], p[1])) - except (IndexError, TypeError): - print( - f"Ignoring deformed point {p}. All points" - " must have at least 2 coordinates." - ) + if isinstance(p, Point): + points.append(p) + else: + try: + points.append(Point(p[0], p[1])) + except (IndexError, TypeError): + print( + f"Ignoring deformed point {p}. All points" + " must have at least 2 coordinates." + ) return points -def _validate_input(points): +def _validate_input(points: Union[List[Point], List[List[float]]]) -> List[Point]: """ validates an input instance before a convex-hull algorithms uses it @@ -168,30 +172,10 @@ def _validate_input(points): if not points: raise ValueError(f"Expecting a list of points but got {points}") - if isinstance(points, set): - points = list(points) - - try: - if hasattr(points, "__iter__") and not isinstance(points[0], Point): - if isinstance(points[0], (list, tuple)): - points = _construct_points(points) - else: - raise ValueError( - "Expecting an iterable of type Point, list or tuple. " - f"Found objects of type {type(points[0])} instead" - ) - elif not hasattr(points, "__iter__"): - raise ValueError( - f"Expecting an iterable object but got an non-iterable type {points}" - ) - except TypeError: - print("Expecting an iterable of type Point, list or tuple.") - raise - - return points + return _construct_points(points) -def _det(a, b, c): +def _det(a: Point, b: Point, c: Point) -> float: """ Computes the sign perpendicular distance of a 2d point c from a line segment ab. The sign indicates the direction of c relative to ab. @@ -226,7 +210,7 @@ def _det(a, b, c): return det -def convex_hull_bf(points): +def convex_hull_bf(points: List[Point]) -> List[Point]: """ Constructs the convex hull of a set of 2D points using a brute force algorithm. The algorithm basically considers all combinations of points (i, j) and uses the @@ -299,7 +283,7 @@ def convex_hull_bf(points): return sorted(convex_set) -def convex_hull_recursive(points): +def convex_hull_recursive(points: List[Point]) -> List[Point]: """ Constructs the convex hull of a set of 2D points using a divide-and-conquer strategy The algorithm exploits the geometric properties of the problem by repeatedly @@ -369,7 +353,7 @@ def convex_hull_recursive(points): return sorted(convex_set) -def _construct_hull(points, left, right, convex_set): +def _construct_hull(points: List[Point], left: Point, right: Point, convex_set: Set[Point]) -> None: """ Parameters @@ -411,7 +395,7 @@ def _construct_hull(points, left, right, convex_set): _construct_hull(candidate_points, extreme_point, right, convex_set) -def convex_hull_melkman(points): +def convex_hull_melkman(points: List[Point]) -> List[Point]: """ Constructs the convex hull of a set of 2D points using the melkman algorithm. The algorithm works by iteratively inserting points of a simple polygonal chain From db5b1faf7c31d1dbc61658f7aa9a9faf3fe3eb28 Mon Sep 17 00:00:00 2001 From: Simon Lammer Date: Thu, 8 Oct 2020 21:01:52 +0200 Subject: [PATCH 5/5] Fix build errors --- divide_and_conquer/convex_hull.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/divide_and_conquer/convex_hull.py b/divide_and_conquer/convex_hull.py index 459b83aa6487..9c096f671385 100644 --- a/divide_and_conquer/convex_hull.py +++ b/divide_and_conquer/convex_hull.py @@ -15,6 +15,7 @@ from typing import Iterable, List, Set, Union + class Point: """ Defines a 2-d point for use by all convex-hull algorithms. @@ -82,7 +83,9 @@ def __hash__(self): return hash(self.x) -def _construct_points(list_of_tuples: Union[List[Point], List[List[float]], Iterable[List[float]]]) -> List[Point]: +def _construct_points( + list_of_tuples: Union[List[Point], List[List[float]], Iterable[List[float]]] +) -> List[Point]: """ constructs a list of points from an array-like object of numbers @@ -111,7 +114,7 @@ def _construct_points(list_of_tuples: Union[List[Point], List[List[float]], Iter [] """ - points = [] + points: List[Point] = [] if list_of_tuples: for p in list_of_tuples: if isinstance(p, Point): @@ -169,10 +172,15 @@ def _validate_input(points: Union[List[Point], List[List[float]]]) -> List[Point ValueError: Expecting an iterable object but got an non-iterable type 1 """ + if not hasattr(points, "__iter__"): + raise ValueError( + f"Expecting an iterable object but got an non-iterable type {points}" + ) + if not points: raise ValueError(f"Expecting a list of points but got {points}") - return _construct_points(points) + return _construct_points(points) def _det(a: Point, b: Point, c: Point) -> float: @@ -353,7 +361,9 @@ def convex_hull_recursive(points: List[Point]) -> List[Point]: return sorted(convex_set) -def _construct_hull(points: List[Point], left: Point, right: Point, convex_set: Set[Point]) -> None: +def _construct_hull( + points: List[Point], left: Point, right: Point, convex_set: Set[Point] +) -> None: """ Parameters