Skip to content

Commit 3825c3e

Browse files
Clear20-22cclausspre-commit-ci[bot]
authored
Add Andrew's Monotone Chain convex hull algorithm (#15165)
* Add Andrew's Monotone Chain convex hull algorithm * Apply suggestion from @cclauss * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent b6224ec commit 3825c3e

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

geometry/monotone_chain.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""
2+
Andrew's Monotone Chain Convex Hull Algorithm.
3+
4+
Reference: https://en.wikipedia.org/wiki/Convex_hull_algorithms#Andrew's_monotone_chain_algorithm
5+
Reference: Andrew, A. M. (1979). "Another efficient algorithm for convex hulls
6+
in two dimensions". Information Processing Letters, 9(5), 216-219.
7+
8+
Andrew's monotone chain algorithm computes the convex hull of a set of 2D points
9+
in O(n log n) time. It first sorts the points lexicographically (by x-coordinate,
10+
and in case of a tie, by y-coordinate) and then constructs the lower and upper
11+
hulls in two separate O(n) passes.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
from typing import NamedTuple
17+
18+
19+
class Point(NamedTuple):
20+
"""
21+
A 2D point with real-valued coordinates.
22+
23+
>>> Point(0.0, 0.0)
24+
Point(x=0.0, y=0.0)
25+
>>> Point(1.5, -2.0)
26+
Point(x=1.5, y=-2.0)
27+
"""
28+
29+
x: float
30+
y: float
31+
32+
33+
def cross_product_direction(origin: Point, point_a: Point, point_b: Point) -> float:
34+
"""
35+
Compute the 2D cross product of vectors (origin -> point_a) and (origin -> point_b).
36+
37+
The return value encodes the orientation of the ordered triplet
38+
(origin, point_a, point_b):
39+
> 0 : Counter-clockwise turn (left turn)
40+
< 0 : Clockwise turn (right turn)
41+
= 0 : Collinear points
42+
43+
Parameters:
44+
origin: The reference pivot point.
45+
point_a: The first endpoint.
46+
point_b: The second endpoint.
47+
48+
Returns:
49+
The signed magnitude of the 2D cross product.
50+
51+
>>> cross_product_direction(Point(0, 0), Point(1, 0), Point(1, 1))
52+
1
53+
>>> cross_product_direction(Point(0, 0), Point(1, 1), Point(1, 0))
54+
-1
55+
>>> cross_product_direction(Point(0, 0), Point(1, 1), Point(2, 2))
56+
0
57+
"""
58+
return (point_a.x - origin.x) * (point_b.y - origin.y) - (point_a.y - origin.y) * (
59+
point_b.x - origin.x
60+
)
61+
62+
63+
def monotone_chain(points: list[Point]) -> list[Point]:
64+
"""
65+
Compute the convex hull of a set of 2D points in counter-clockwise order
66+
using Andrew's Monotone Chain algorithm.
67+
68+
Parameters:
69+
points: A list of 2D points.
70+
71+
Returns:
72+
A list of vertices forming the convex hull in counter-clockwise order.
73+
74+
Time Complexity: O(n log n) where n is the number of points.
75+
Space Complexity: O(n)
76+
77+
Examples:
78+
>>> monotone_chain([])
79+
[]
80+
>>> monotone_chain([Point(1, 1)])
81+
[Point(x=1, y=1)]
82+
>>> monotone_chain([Point(0, 0), Point(1, 1)])
83+
[Point(x=0, y=0), Point(x=1, y=1)]
84+
>>> square_points = [
85+
... Point(0, 0), Point(2, 0), Point(2, 2), Point(0, 2),
86+
... Point(1, 1), Point(1, 0.5)
87+
... ]
88+
>>> monotone_chain(square_points)
89+
[Point(x=0, y=0), Point(x=2, y=0), Point(x=2, y=2), Point(x=0, y=2)]
90+
>>> triangle_with_duplicates = [
91+
... Point(0, 0), Point(4, 0), Point(2, 3),
92+
... Point(0, 0), Point(4, 0), Point(2, 1)
93+
... ]
94+
>>> monotone_chain(triangle_with_duplicates)
95+
[Point(x=0, y=0), Point(x=4, y=0), Point(x=2, y=3)]
96+
>>> collinear_points = [Point(0, 0), Point(1, 1), Point(2, 2), Point(3, 3)]
97+
>>> monotone_chain(collinear_points)
98+
[Point(x=0, y=0), Point(x=3, y=3)]
99+
"""
100+
unique_sorted_points = sorted(set(points))
101+
if len(unique_sorted_points) <= 1:
102+
return unique_sorted_points
103+
104+
# Build the lower hull: only keep counter-clockwise turns
105+
lower_hull: list[Point] = []
106+
for candidate_point in unique_sorted_points:
107+
while (
108+
len(lower_hull) >= 2
109+
and cross_product_direction(lower_hull[-2], lower_hull[-1], candidate_point)
110+
<= 0
111+
):
112+
lower_hull.pop()
113+
lower_hull.append(candidate_point)
114+
115+
# Build the upper hull: only keep counter-clockwise turns
116+
upper_hull: list[Point] = []
117+
for candidate_point in reversed(unique_sorted_points):
118+
while (
119+
len(upper_hull) >= 2
120+
and cross_product_direction(upper_hull[-2], upper_hull[-1], candidate_point)
121+
<= 0
122+
):
123+
upper_hull.pop()
124+
upper_hull.append(candidate_point)
125+
126+
# Omit the last point of each half because it is repeated at the ends
127+
return lower_hull[:-1] + upper_hull[:-1]
128+
129+
130+
if __name__ == "__main__":
131+
import doctest
132+
133+
doctest.testmod()

0 commit comments

Comments
 (0)