Skip to content

Commit 52bf749

Browse files
authored
Merge branch 'master' into fix/int-to-roman-invalid-input-validation
2 parents 96d953e + e3b01ec commit 52bf749

14 files changed

Lines changed: 462 additions & 17 deletions

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
runs-on: ubuntu-latest
1111
steps:
1212
- run: sudo apt-get update && sudo apt-get install -y libhdf5-dev
13-
- uses: actions/checkout@v6
13+
- uses: actions/checkout@v7
1414
- uses: astral-sh/setup-uv@v7
1515
with:
1616
enable-cache: true

.github/workflows/devcontainer_ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
build:
1313
runs-on: ubuntu-latest
1414
steps:
15-
- uses: actions/checkout@v6
15+
- uses: actions/checkout@v7
1616
- uses: devcontainers/ci@v0.3
1717
with:
1818
push: never

.github/workflows/directory_writer.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ jobs:
66
directory_writer:
77
runs-on: ubuntu-latest
88
steps:
9-
- uses: actions/checkout@v6
9+
- uses: actions/checkout@v7
1010
with:
1111
fetch-depth: 0
1212
- uses: actions/setup-python@v6

.github/workflows/project_euler.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
libxml2-dev libxslt-dev
2222
libhdf5-dev
2323
libopenblas-dev
24-
- uses: actions/checkout@v6
24+
- uses: actions/checkout@v7
2525
- uses: astral-sh/setup-uv@v7
2626
- uses: actions/setup-python@v6
2727
with:
@@ -39,7 +39,7 @@ jobs:
3939
libxml2-dev libxslt-dev
4040
libhdf5-dev
4141
libopenblas-dev
42-
- uses: actions/checkout@v6
42+
- uses: actions/checkout@v7
4343
- uses: astral-sh/setup-uv@v7
4444
- uses: actions/setup-python@v6
4545
with:

.github/workflows/ruff.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ jobs:
1111
ruff:
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v6
14+
- uses: actions/checkout@v7
1515
- uses: astral-sh/setup-uv@v7
1616
- run: uvx ruff check --output-format=github .

.github/workflows/sphinx.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ jobs:
3232
libxml2-dev libxslt-dev
3333
libhdf5-dev
3434
libopenblas-dev
35-
- uses: actions/checkout@v6
35+
- uses: actions/checkout@v7
3636
- uses: astral-sh/setup-uv@v7
3737
- uses: actions/setup-python@v6
3838
with:

.pre-commit-config.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ repos:
1919
- id: auto-walrus
2020

2121
- repo: https://github.com/astral-sh/ruff-pre-commit
22-
rev: v0.15.9
22+
rev: v0.15.15
2323
hooks:
2424
- id: ruff-check
2525
- id: ruff-format
@@ -32,7 +32,7 @@ repos:
3232
- tomli
3333

3434
- repo: https://github.com/tox-dev/pyproject-fmt
35-
rev: v2.21.0
35+
rev: v2.23.0
3636
hooks:
3737
- id: pyproject-fmt
3838

DIRECTORY.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,6 +471,8 @@
471471
* [Geometry](geometry/geometry.py)
472472
* [Graham Scan](geometry/graham_scan.py)
473473
* [Jarvis March](geometry/jarvis_march.py)
474+
* [Ramer Douglas Peucker](geometry/ramer_douglas_peucker.py)
475+
* [Segment Intersection](geometry/segment_intersection.py)
474476
* Tests
475477
* [Test Graham Scan](geometry/tests/test_graham_scan.py)
476478
* [Test Jarvis March](geometry/tests/test_jarvis_march.py)
@@ -523,6 +525,7 @@
523525
* [Graphs Floyd Warshall](graphs/graphs_floyd_warshall.py)
524526
* [Greedy Best First](graphs/greedy_best_first.py)
525527
* [Greedy Min Vertex Cover](graphs/greedy_min_vertex_cover.py)
528+
* [Johnson](graphs/johnson.py)
526529
* [Kahns Algorithm Long](graphs/kahns_algorithm_long.py)
527530
* [Kahns Algorithm Topo](graphs/kahns_algorithm_topo.py)
528531
* [Karger](graphs/karger.py)
@@ -543,6 +546,7 @@
543546
* [Strongly Connected Components](graphs/strongly_connected_components.py)
544547
* [Tarjans Scc](graphs/tarjans_scc.py)
545548
* Tests
549+
* [Test Johnson](graphs/tests/test_johnson.py)
546550
* [Test Min Spanning Tree Kruskal](graphs/tests/test_min_spanning_tree_kruskal.py)
547551
* [Test Min Spanning Tree Prim](graphs/tests/test_min_spanning_tree_prim.py)
548552

geometry/ramer_douglas_peucker.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
"""
2+
Ramer-Douglas-Peucker polyline simplification algorithm.
3+
4+
Given a sequence of 2-D points and a tolerance epsilon, the algorithm
5+
reduces the number of points while preserving the overall shape of the curve.
6+
7+
Time complexity: O(n log n) average, O(n²) worst case
8+
Space complexity: O(n)
9+
10+
References:
11+
https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import math
17+
18+
19+
def _euclidean_distance(
20+
point_a: tuple[float, float],
21+
point_b: tuple[float, float],
22+
) -> float:
23+
"""Return the Euclidean distance between two 2-D points.
24+
25+
>>> _euclidean_distance((0.0, 0.0), (3.0, 4.0))
26+
5.0
27+
>>> _euclidean_distance((1.0, 1.0), (1.0, 1.0))
28+
0.0
29+
"""
30+
return math.hypot(point_b[0] - point_a[0], point_b[1] - point_a[1])
31+
32+
33+
def _perpendicular_distance(
34+
point: tuple[float, float],
35+
line_start: tuple[float, float],
36+
line_end: tuple[float, float],
37+
) -> float:
38+
"""Return the distance from *point* to the line **segment** between
39+
*line_start* and *line_end*.
40+
41+
When the perpendicular projection of *point* onto the infinite line falls
42+
within the segment, this equals the perpendicular distance to that line.
43+
When the projection falls outside the segment, the distance to the nearest
44+
endpoint is returned instead (projection clamped to [0, 1]).
45+
46+
This is the correct distance measure for the Ramer-Douglas-Peucker
47+
algorithm: using the infinite-line distance can incorrectly discard points
48+
whose projection lies beyond a segment endpoint.
49+
50+
>>> _perpendicular_distance((4.0, 0.0), (0.0, 0.0), (0.0, 3.0))
51+
4.0
52+
>>> # order of line_start and line_end does not affect the result
53+
>>> _perpendicular_distance((4.0, 0.0), (0.0, 3.0), (0.0, 0.0))
54+
4.0
55+
>>> _perpendicular_distance((4.0, 1.0), (0.0, 1.0), (0.0, 4.0))
56+
4.0
57+
>>> _perpendicular_distance((2.0, 1.0), (-2.0, 1.0), (-2.0, 4.0))
58+
4.0
59+
>>> # projection falls outside the segment; distance to nearest endpoint
60+
>>> round(_perpendicular_distance((0.0, 2.0), (1.0, 0.0), (3.0, 0.0)), 6)
61+
2.236068
62+
"""
63+
px, py = point
64+
ax, ay = line_start
65+
bx, by = line_end
66+
dx, dy = bx - ax, by - ay
67+
seg_len_sq = dx * dx + dy * dy
68+
if seg_len_sq == 0.0:
69+
# line_start and line_end coincide; fall back to point-to-point distance
70+
return _euclidean_distance(point, line_start)
71+
# Project point onto the segment line, then clamp t to [0, 1] so the
72+
# nearest point is always on the segment rather than the infinite line.
73+
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / seg_len_sq))
74+
nearest_x = ax + t * dx
75+
nearest_y = ay + t * dy
76+
return math.hypot(px - nearest_x, py - nearest_y)
77+
78+
79+
def ramer_douglas_peucker(
80+
pts: list[tuple[float, float]],
81+
epsilon: float,
82+
) -> list[tuple[float, float]]:
83+
"""Simplify a polyline using the Ramer-Douglas-Peucker algorithm.
84+
85+
Given a sequence of 2-D points and a maximum allowable deviation
86+
*epsilon* (>= 0), returns a simplified list of points such that no
87+
discarded point is farther than *epsilon* from the simplified polyline.
88+
89+
Parameters
90+
----------
91+
pts:
92+
Ordered sequence of ``(x, y)`` points describing the polyline.
93+
epsilon:
94+
Maximum allowable distance of any discarded point from the
95+
simplified polyline. Must be non-negative.
96+
97+
Returns
98+
-------
99+
list[tuple[float, float]]
100+
Simplified list of ``(x, y)`` points. The first and last points of
101+
*pts* are always preserved.
102+
103+
Raises
104+
------
105+
ValueError
106+
If *epsilon* is negative.
107+
108+
References
109+
----------
110+
https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
111+
112+
Examples
113+
--------
114+
>>> ramer_douglas_peucker([], epsilon=1.0)
115+
[]
116+
>>> ramer_douglas_peucker([(0.0, 0.0)], epsilon=1.0)
117+
[(0.0, 0.0)]
118+
>>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.0)], epsilon=1.0)
119+
[(0.0, 0.0), (1.0, 0.0)]
120+
>>> # middle point is within epsilon - it is discarded
121+
>>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.1), (2.0, 0.0)], epsilon=0.5)
122+
[(0.0, 0.0), (2.0, 0.0)]
123+
>>> # middle point exceeds epsilon - it is kept
124+
>>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)], epsilon=0.5)
125+
[(0.0, 0.0), (1.0, 1.0), (2.0, 0.0)]
126+
>>> ramer_douglas_peucker([(0.0, 0.0), (1.0, 0.5), (2.0, 0.0)], epsilon=-1.0)
127+
Traceback (most recent call last):
128+
...
129+
ValueError: epsilon must be non-negative, got -1.0
130+
"""
131+
if epsilon < 0:
132+
msg = f"epsilon must be non-negative, got {epsilon!r}"
133+
raise ValueError(msg)
134+
135+
if len(pts) < 3:
136+
return list(pts)
137+
138+
# ---------------------------------------------------------------------------
139+
# Iterative, stack-based implementation.
140+
#
141+
# The naive recursive approach copies sublists at every level via slicing
142+
# (pts[:max_index+1] / pts[max_index:]), which is O(n) per call and makes
143+
# the overall algorithm O(n²) in memory even for well-balanced splits. An
144+
# explicit stack operating on index ranges avoids all copying and also
145+
# eliminates the risk of hitting Python's recursion limit for long polylines.
146+
# ---------------------------------------------------------------------------
147+
n = len(pts)
148+
149+
# keep[i] is True when pts[i] must appear in the output.
150+
keep: list[bool] = [False] * n
151+
keep[0] = True
152+
keep[-1] = True
153+
154+
# Stack of (start_index, end_index) pairs still to be examined.
155+
stack: list[tuple[int, int]] = [(0, n - 1)]
156+
157+
while stack:
158+
start, end = stack.pop()
159+
if end - start < 2:
160+
# Only one interior candidate at most; nothing to split further.
161+
continue
162+
163+
# Find the interior point with the greatest distance to the segment.
164+
max_dist = 0.0
165+
max_index = start
166+
for i in range(start + 1, end):
167+
dist = _perpendicular_distance(pts[i], pts[start], pts[end])
168+
if dist > max_dist:
169+
max_dist = dist
170+
max_index = i
171+
172+
if max_dist > epsilon:
173+
keep[max_index] = True
174+
stack.append((start, max_index))
175+
stack.append((max_index, end))
176+
# else: all interior points are within epsilon; discard them all.
177+
178+
return [pts[i] for i in range(n) if keep[i]]
179+
180+
181+
if __name__ == "__main__":
182+
import doctest
183+
184+
doctest.testmod()

0 commit comments

Comments
 (0)