-
-
Notifications
You must be signed in to change notification settings - Fork 51k
Add Andrew's Monotone Chain convex hull algorithm #15165
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
c64c1d4
52c7c34
30eb39f
a5d7486
65a8955
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| """ | ||
| Andrew's Monotone Chain Convex Hull Algorithm. | ||
|
|
||
| Reference: https://en.wikipedia.org/wiki/Convex_hull_algorithms#Andrew's_monotone_chain_algorithm | ||
| Reference: Andrew, A. M. (1979). "Another efficient algorithm for convex hulls | ||
| in two dimensions". Information Processing Letters, 9(5), 216-219. | ||
|
|
||
| Andrew's monotone chain algorithm computes the convex hull of a set of 2D points | ||
| in O(n log n) time. It first sorts the points lexicographically (by x-coordinate, | ||
| and in case of a tie, by y-coordinate) and then constructs the lower and upper | ||
| hulls in two separate O(n) passes. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import NamedTuple | ||
|
|
||
|
|
||
| class Point(NamedTuple): | ||
| """ | ||
| A 2D point with real-valued coordinates. | ||
|
|
||
| >>> Point(0.0, 0.0) | ||
| Point(x=0.0, y=0.0) | ||
| >>> Point(1.5, -2.0) | ||
| Point(x=1.5, y=-2.0) | ||
| """ | ||
|
|
||
| x: float | ||
| y: float | ||
|
|
||
|
|
||
| def cross_product_direction(origin: Point, point_a: Point, point_b: Point) -> float: | ||
| """ | ||
| Compute the 2D cross product of vectors (origin -> point_a) and (origin -> point_b). | ||
|
|
||
| The return value encodes the orientation of the ordered triplet | ||
| (origin, point_a, point_b): | ||
| > 0 : Counter-clockwise turn (left turn) | ||
| < 0 : Clockwise turn (right turn) | ||
| = 0 : Collinear points | ||
|
|
||
| Parameters: | ||
| origin: The reference pivot point. | ||
| point_a: The first endpoint. | ||
| point_b: The second endpoint. | ||
|
|
||
| Returns: | ||
| The signed magnitude of the 2D cross product. | ||
|
|
||
| >>> cross_product_direction(Point(0, 0), Point(1, 0), Point(1, 1)) | ||
| 1 | ||
| >>> cross_product_direction(Point(0, 0), Point(1, 1), Point(1, 0)) | ||
| -1 | ||
| >>> cross_product_direction(Point(0, 0), Point(1, 1), Point(2, 2)) | ||
| 0 | ||
| """ | ||
| return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * ( | ||
| point_b.x - origin.x | ||
| ) | ||
|
|
||
|
|
||
| def monotone_chain(points: list[Point]) -> list[Point]: | ||
| """ | ||
| Compute the convex hull of a set of 2D points in counter-clockwise order | ||
| using Andrew's Monotone Chain algorithm. | ||
|
|
||
| Parameters: | ||
| points: A list of 2D points. | ||
|
|
||
| Returns: | ||
| A list of vertices forming the convex hull in counter-clockwise order. | ||
|
|
||
| Time Complexity: O(n log n) where n is the number of points. | ||
| Space Complexity: O(n) | ||
|
|
||
| Examples: | ||
| >>> monotone_chain([]) | ||
| [] | ||
| >>> monotone_chain([Point(1, 1)]) | ||
| [Point(x=1, y=1)] | ||
| >>> monotone_chain([Point(0, 0), Point(1, 1)]) | ||
| [Point(x=0, y=0), Point(x=1, y=1)] | ||
| >>> square_points = [ | ||
| ... Point(0, 0), Point(2, 0), Point(2, 2), Point(0, 2), | ||
| ... Point(1, 1), Point(1, 0.5) | ||
| ... ] | ||
| >>> monotone_chain(square_points) | ||
| [Point(x=0, y=0), Point(x=2, y=0), Point(x=2, y=2), Point(x=0, y=2)] | ||
| >>> triangle_with_duplicates = [ | ||
| ... Point(0, 0), Point(4, 0), Point(2, 3), | ||
| ... Point(0, 0), Point(4, 0), Point(2, 1) | ||
| ... ] | ||
| >>> monotone_chain(triangle_with_duplicates) | ||
| [Point(x=0, y=0), Point(x=4, y=0), Point(x=2, y=3)] | ||
| >>> collinear_points = [Point(0, 0), Point(1, 1), Point(2, 2), Point(3, 3)] | ||
| >>> monotone_chain(collinear_points) | ||
| [Point(x=0, y=0), Point(x=3, y=3)] | ||
| """ | ||
| unique_sorted_points = sorted(set(points)) | ||
|
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. Smart use of set() to deduplicate points before sorting — this One thing worth noting: set() on a NamedTuple works because |
||
| if len(unique_sorted_points) <= 1: | ||
| return unique_sorted_points | ||
|
|
||
| # Build the lower hull: only keep counter-clockwise turns | ||
| lower_hull: list[Point] = [] | ||
| for candidate_point in unique_sorted_points: | ||
| while ( | ||
| len(lower_hull) >= 2 | ||
| and cross_product_direction(lower_hull[-2], lower_hull[-1], candidate_point) | ||
| <= 0 | ||
| ): | ||
| lower_hull.pop() | ||
| lower_hull.append(candidate_point) | ||
|
|
||
| # Build the upper hull: only keep counter-clockwise turns | ||
| upper_hull: list[Point] = [] | ||
| for candidate_point in reversed(unique_sorted_points): | ||
| while ( | ||
| len(upper_hull) >= 2 | ||
| and cross_product_direction(upper_hull[-2], upper_hull[-1], candidate_point) | ||
| <= 0 | ||
| ): | ||
| upper_hull.pop() | ||
| upper_hull.append(candidate_point) | ||
|
|
||
| # Omit the last point of each half because it is repeated at the ends | ||
| return lower_hull[:-1] + upper_hull[:-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. The slicing to omit the last point of each half is correct — Worth adding a brief inline comment explaining why both hulls Drop last point of each hull to avoid duplicatingthe leftmost and rightmost points where hulls meetreturn lower_hull[:-1] + upper_hull[:-1] |
||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| import doctest | ||
|
|
||
| doctest.testmod() | ||
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.
The cross product formula is correct and well-documented above.
Consider adding a one-line summary of what the return value
means directly in the docstring Examples section for quick
reference:
Positive = counter-clockwise, Negative = clockwise, Zero = collinear
This would help readers verify the doctests on lines 51-56
without having to re-read the full orientation explanation.