Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Computational Geometry  ›  Lesson

The Sweep Line

Sweep Line 34 minA vertical line sweeps left to right across every segment's endpoints; an ordered active set tracks which segments the sweep is currently crossing, so only NEIGHBOURS in that order ever need an intersection test, not every pair
You're building a piece ofMeridian — service areas & coverage
This piece — any_intersection(): Sweeps a line across a whole batch of routes to flag any crossing, without testing every pair.
Scenario A whole batch of new routes needs a single yes/no: does ANY pair of them cross? With dozens of routes, testing every pair gets expensive fast — Meridian needs an answer that doesn't grow like n squared.
Your task
Build any_intersection(segments). Return True if ANY two segments in the list cross or overlap. Sort every segment's endpoints into events (a 'start' event at the left endpoint, an 'end' event at the right endpoint), then process events left to right. Keep an 'order' list of currently-active segment indices, kept sorted by each segment's y-value at the current sweep position (seg_y_at). On a start event, insert the segment into 'order' at the position bisect finds, then test ONLY its new neighbours (the segment immediately above and below it in 'order') for an intersection. On an end event, remove the segment from 'order'. Return True the moment a neighbour test finds a crossing; return False if the sweep finishes clean. Example: any_intersection([((0,0),(10,0)), ((0,2),(10,2)), ((0,4),(10,4)), ((1,5),(1,-1))]) -> True (the vertical segment crosses all three horizontal ones).

The Sweep Line

In short: to find a crossing among many segments, you don't have to check every pair. Sweep a vertical line left to right. At any moment, the line is only crossing some subset of the segments — the active set — and that set has a real order: top to bottom, by where each segment currently sits. Keep the active set sorted (section 14's balanced structures do this with an O(log n) insert; a sorted list with bisect is fine here) and test a newly-inserted segment against only its two neighbours in that order. Anything farther away has a neighbour sitting between them that would have to cross first. The tested-pair count stays far below n², because every insertion costs at most two tests, however many segments are already active.

A vertical line partway across a city grid, with three horizontal avenues and one vertical street it currently crosses, the street's insertion point on a side panel showing it slotting in between the two avenues immediately above and below it, with only those two getting a tested badge and the far avenue greyed out

Three avenues, three cross streets

Meridian has three horizontal avenues and three north-south cross streets. Does any cross street actually meet an avenue?

step through it
1import bisect
2 
3def orientation(a, b, c):
4 cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
5 if cross > 0:
6 return 1
7 if cross < 0:
8 return -1
9 return 0
10 
11def on_segment(p, a, b):
12 return (min(a[0], b[0]) <= p[0] <= max(a[0], b[0]) and
13 min(a[1], b[1]) <= p[1] <= max(a[1], b[1]))
14 
15def segments_intersect(p1, p2, p3, p4):
16 d1 = orientation(p3, p4, p1)
17 d2 = orientation(p3, p4, p2)
18 d3 = orientation(p1, p2, p3)
19 d4 = orientation(p1, p2, p4)
20 if d1 != d2 and d3 != d4:
21 return True
22 if d1 == 0 and on_segment(p1, p3, p4):
23 return True
24 if d2 == 0 and on_segment(p2, p3, p4):
25 return True
26 if d3 == 0 and on_segment(p3, p1, p2):
27 return True
28 if d4 == 0 and on_segment(p4, p1, p2):
29 return True
30 return False
31 
32def seg_y_at(seg, x):
33 (x1, y1), (x2, y2) = seg
34 if x1 == x2:
35 return y1
36 t = (x - x1) / (x2 - x1)
37 return y1 + t * (y2 - y1)
38 
39def events_for(segments):
40 events = []
41 for i, (p, q) in enumerate(segments):
42 if p[0] > q[0]:
43 p, q = q, p
44 events.append((p[0], 0, i))
45 events.append((q[0], 1, i))
46 events.sort()
47 return events
48 
49def any_intersection(segments):
50 events = events_for(segments)
51 order = []
52 for x, kind_rank, idx in events:
53 if kind_rank == 0:
54 key = seg_y_at(segments[idx], x)
55 ys = [seg_y_at(segments[j], x) for j in order]
56 pos = bisect.bisect_left(ys, key)
57 order.insert(pos, idx)
58 for j in (order[pos - 1] if pos > 0 else None,
59 order[pos + 1] if pos < len(order) - 1 else None):
60 if j is not None and segments_intersect(*segments[idx], *segments[j]):
61 return True
62 else:
63 if idx in order:
64 order.pop(order.index(idx))
65 return False
66 
67routes = [
68 ((0, 0), (10, 0)), ((0, 2), (10, 2)), ((0, 4), (10, 4)), # three avenues
69 ((1, 5), (1, -1)), ((5, 5), (5, -1)), ((9, 5), (9, -1)), # three cross streets
70]
71print(any_intersection(routes))

The three avenues all start at x = 0 and go into the active set together, ordered by their y-values: [0, 2, 4]. The first cross street starts at x = 1 and inserts itself between the avenue at y=4 and... nothing above it, so it's tested only against its immediate neighbour, not all three avenues. That single test already finds a crossing, and the sweep returns True immediately — it never even reaches the other two cross streets.

Counting how few pairs get tested

The same six routes, brute force, would check 6 * 5 / 2 = 15 pairs. The sweep tests far fewer, because it only ever compares adjacent segments at the moment one is inserted:

python

5 tests instead of 15 — and the gap only widens as more routes get added, because brute force grows with the square of the route count while the sweep's tests per route stay capped at 2.

Why the active set needs to be ordered

If order were just an unsorted list, "the neighbours" would mean nothing — you'd be back to testing a new segment against everyone active, which is brute force again, just delayed. The active set's order has to track the sweep line's current crossing position, which changes continuously as segments rise and fall relative to each other. A sorted container — a balanced tree, a skip list, or (for teaching) a plain list kept sorted with bisect — is what makes "neighbour" a meaningful, cheap-to-find idea.

Quick practice

What does this print?
import bisect
ys = [0, 2, 4]
pos = bisect.bisect_left(ys, 5)
print(pos)
What does this print?
order = [10, 20, 30, 40]
pos = 2
neighbours = []
if pos > 0:
    neighbours.append(order[pos - 1])
if pos < len(order) - 1:
    neighbours.append(order[pos + 1])
print(neighbours)

🎯 Your turn

Build any_intersection(segments): sweep left to right, keep the active set ordered by current y, and test only neighbours.

  • Three avenues plus a crossing street → True
  • Three parallel avenues alone → False
  • [] → False

Hint: build (x, 0/1, index) events for every start/end, sort them, then on a start event use bisect.bisect_left on the active segments' current y-values to find where to insert, and test only order[pos-1] and order[pos+1].

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upWhere is this segment right now?

Write seg_y_at(seg, x), where seg is (p, q) with p[0] <= q[0]. Return the segment's y-value at the given x (linear interpolation along the segment). If the segment is vertical (p[0] == q[0]), return p[1]. seg_y_at(((0,0), (10,10)), 5) -> 5.0.

sweep-line
DrillInsert into a sorted list

Write ordered_insert(ys, y), returning a NEW list with y inserted into the already-sorted list ys, keeping it sorted, using bisect. This is the ordered active set's basic move from section 14, applied here to y-values instead of keys in a tree. ordered_insert([1, 3, 5, 8], 4) -> [1, 3, 4, 5, 8].

balanced-treessweep-line
BuildOnly the neighbours matter

Write neighbours(order, pos), returning a list with the element immediately BEFORE pos and the element immediately AFTER pos in order — whichever of those exist. This is exactly who a newly-inserted segment at pos needs to be tested against. neighbours([10, 20, 30, 40], 1) -> [10, 30].

sweep-line
BossHow few pairs actually get tested?

Write count_tested_pairs(segments), running the same sweep as any_intersection but, instead of stopping at the first crossing, counting every neighbour test performed (whether or not it found a crossing) and returning that count. count_tested_pairs([((0,0),(10,0)), ((0,2),(10,2)), ((0,4),(10,4)), ((1,5),(1,-1)), ((5,5),(5,-1)), ((9,5),(9,-1))]) -> 5.

sweep-linecomplexity
CapstoneWhich pair crosses first, by sweep position?

Write first_intersection(segments), returning the first pair of indices [i, j] (sorted, i < j) that the sweep finds crossing, or None if the sweep finishes clean. Use the same active-set sweep as any_intersection, but return the pair instead of just True. first_intersection([((0,0),(10,0)), ((0,2),(10,2)), ((0,4),(10,4)), ((1,5),(1,-1))]) -> [2, 3].

sweep-linesegment-intersection
any_intersection.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
any_intersection(segments) → boolTrue if any two of the segments cross or overlap, found by sweeping an ordered active set instead of checking every pair.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.