Rectangle intersection sweep in Python
Sweep over x-intervals of rectangles, checking y-overlap against active rectangles.
- File
rectangle_intersection_sweep.py, 65 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-intersection-sweep/. The authors are listed in prov/KGSR21.jsonl.
from bisect import bisect_left, insort
from typing import Sequence
Rect = tuple[float, float, float, float] # (x1, y1, x2, y2) with x1 <= x2 and y1 <= y2
def find_intersecting_pair(rects: Sequence[Rect]) -> tuple[int, int] | None:
"""Return indices (i, j), i < j, of two closed rectangles that intersect, or None.
Each rectangle is (x1, y1, x2, y2) and stands for [x1, x2] x [y1, y2].
Rectangles that touch on an edge or a corner intersect. The sweep sorts
2n events in O(n log n) time. The active set is a sorted Python list, so
an insert or a delete costs O(n) moves; a balanced search tree makes each
one O(log n) and the whole sweep O(n log n).
"""
events = []
for i, (x1, _, x2, _) in enumerate(rects): # @line L1
events.append((x1, 0, i))
events.append((x2, 1, i))
events.sort() # @line L2
active: list[tuple[float, int]] = [] # @line L3
for x, kind, i in events: # @line L4
y1, y2 = rects[i][1], rects[i][3]
key = (y1, i)
if kind == 0: # @line L5
insort(active, key) # @line L6
pos = bisect_left(active, key)
p = active[pos - 1][1] if pos > 0 else None # @line L7
s = active[pos + 1][1] if pos + 1 < len(active) else None
if p is not None and rects[p][3] >= y1: # @line L8
return (min(p, i), max(p, i))
if s is not None and rects[s][1] <= y2: # @line L9
return (min(i, s), max(i, s))
else:
del active[bisect_left(active, key)] # @line L10
return None # @line L11
def all_intersecting_pairs(rects: Sequence[Rect]) -> list[tuple[int, int]]:
"""Return every pair (i, j), i < j, of intersecting closed rectangles, sorted.
The same sweep, but each new rectangle is tested against every active
one. This costs O(n log n + n * a) time, where a is the largest number of
active rectangles; an interval tree gives O(n log n + k) for k pairs.
"""
events = []
for i, (x1, _, x2, _) in enumerate(rects):
events.append((x1, 0, i))
events.append((x2, 1, i))
events.sort()
active: set[int] = set()
out = []
for _, kind, i in events:
if kind == 0:
y1, y2 = rects[i][1], rects[i][3]
for j in active:
if rects[j][1] <= y2 and y1 <= rects[j][3]:
out.append((min(i, j), max(i, j)))
active.add(i)
else:
active.discard(i)
return sorted(out)
test_rectangle_intersection_sweep.py 55 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/rectangle-intersection-sweep/. The authors are listed in prov/KGSR21.jsonl.
import random
from rectangle_intersection_sweep import all_intersecting_pairs, find_intersecting_pair
def meets(a, b) -> bool:
return a[0] <= b[2] and b[0] <= a[2] and a[1] <= b[3] and b[1] <= a[3]
def brute(rects):
n = len(rects)
return [(i, j) for i in range(n) for j in range(i + 1, n) if meets(rects[i], rects[j])]
EXAMPLE = [(1, 1, 3, 2), (2, 4, 5, 6), (4, 0, 7, 3), (6, 5, 9, 7), (8, 1, 10, 4)]
def test_example() -> None:
assert find_intersecting_pair(EXAMPLE) is None
moved = EXAMPLE + [(5, 3, 6, 4)]
assert find_intersecting_pair(moved) == (2, 5)
assert all_intersecting_pairs(moved) == [(1, 5), (2, 5)]
def test_edges() -> None:
assert find_intersecting_pair([]) is None
assert find_intersecting_pair([(0, 0, 1, 1)]) is None
assert find_intersecting_pair([(0, 0, 1, 1), (1, 1, 2, 2)]) == (0, 1) # corner touch
assert find_intersecting_pair([(0, 0, 1, 1), (1, 2, 2, 3)]) is None
assert find_intersecting_pair([(0, 0, 1, 1), (0, 0, 1, 1)]) == (0, 1) # identical
assert find_intersecting_pair([(0, 0, 10, 10), (2, 2, 3, 3)]) == (0, 1) # nested
# a long thin rectangle crosses a tall thin one: no corner is inside the other
assert find_intersecting_pair([(0, 4, 10, 5), (4, 0, 5, 10)]) == (0, 1)
# equal lower edges: the key (y1, index) breaks the tie
assert find_intersecting_pair([(0, 2, 1, 3), (5, 2, 6, 3), (5.5, 2, 7, 2.5)]) == (1, 2)
def test_against_brute_force() -> None:
rng = random.Random(7)
for _ in range(2000):
n = rng.randrange(0, 9)
rects = []
for _ in range(n):
x1, y1 = rng.randrange(12), rng.randrange(12)
rects.append((x1, y1, x1 + rng.randrange(0, 4), y1 + rng.randrange(0, 4)))
want = brute(rects)
got = find_intersecting_pair(rects)
if want:
assert got is not None and meets(rects[got[0]], rects[got[1]]) and got[0] != got[1]
else:
assert got is None
assert all_intersecting_pairs(rects) == want
What the next level needs
- the tests did not run in this build