πŸ¦€ Functional Rust

839: Sweep Line Algorithm with Event Queue

Difficulty: 4 Level: Advanced Process geometric or temporal events sorted by position β€” the algorithmic skeleton behind interval union, rectangle area, and line intersection detection.

The Problem This Solves

Many geometric and temporal problems can be solved by imagining a vertical line sweeping from left to right across the plane, processing events (interval starts, ends, intersections) as the line encounters them. Rather than examining all pairs of objects (O(nΒ²)), the sweep line maintains an ordered state of "currently active" objects and updates it as events occur β€” often reducing complexity to O(n log n). Sweep line algorithms solve: computing the total length/area of a union of intervals/rectangles, reporting all intersecting line segments (Bentley-Ottmann), computing Voronoi diagrams (Fortune's algorithm), and scheduling problems (earliest deadline first, interval colouring). It's the algorithmic paradigm behind many O(n log n) geometry algorithms that would naΓ―vely be O(nΒ²). This example demonstrates sweep line on the interval union problem: given n intervals, compute the total length covered (counting overlaps only once). It also shows the skeleton for more general event processing.

The Intuition

Sort all interval endpoints as events: START events at the left endpoint, END events at the right endpoint. Process events left to right, maintaining a counter of "active intervals." When the counter goes from 0β†’1 (entering covered territory), start a new covered segment. When it goes from 1β†’0 (leaving covered territory), close the segment. For the general case, the "state" can be a balanced BST of active segments (ordered by y-coordinate at the current sweep x), with START inserting into the BST, END removing from it, and INTERSECTION events causing adjacent segments to swap order. The event-driven approach is O(n log n): sort events O(n log n), process each event O(log n) with a priority queue and active set.

How It Works in Rust

#[derive(Debug, Clone, PartialEq)]
enum EventType { Start, End }

#[derive(Debug, Clone)]
struct Event {
 x: f64,
 kind: EventType,
 id: usize,
}

fn sweep_line_union(intervals: &[(f64, f64)]) -> f64 {
 if intervals.is_empty() { return 0.0; }

 // Create START and END events for each interval
 let mut events: Vec<Event> = intervals.iter().enumerate()
     .flat_map(|(id, &(lo, hi))| {
         [
             Event { x: lo, kind: EventType::Start, id },
             Event { x: hi, kind: EventType::End,   id },
         ]
     })
     .collect();

 // Sort: by x, with END before START at the same x (avoids counting 0-gap)
 events.sort_by(|a, b| {
     a.x.partial_cmp(&b.x).unwrap()
         .then_with(|| {
             // At same x: process End before Start to avoid double-counting
             match (&a.kind, &b.kind) {
                 (EventType::End, EventType::Start) => std::cmp::Ordering::Less,
                 (EventType::Start, EventType::End) => std::cmp::Ordering::Greater,
                 _ => std::cmp::Ordering::Equal,
             }
         })
 });

 let mut total = 0.0;
 let mut active = 0usize;  // count of active (overlapping) intervals
 let mut cover_start = 0.0; // x where current covered segment started

 for event in &events {
     match event.kind {
         EventType::Start => {
             if active == 0 {
                 cover_start = event.x; // entering covered territory
             }
             active += 1;
         }
         EventType::End => {
             active -= 1;
             if active == 0 {
                 total += event.x - cover_start; // leaving covered territory
             }
         }
     }
 }
 total
}

// Generic event processor skeleton
fn process_events<S, F>(events: &mut Vec<Event>, initial_state: S, handler: F) -> S
where
 F: Fn(S, &Event) -> S,
{
 events.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
 events.iter().fold(initial_state, handler)
}
The tie-breaking rule (END before START at equal x) is critical: two intervals [1, 3] and [3, 5] share endpoint x=3. Without the tie-break, you'd count a gap at 3 that doesn't exist. `then_with` on `Ordering` chains comparisons cleanly β€” compare by x first, then by event type to break ties. This is the idiomatic Rust multi-key sort pattern. The `process_events` skeleton shows how sweep line generalises: `fold` over sorted events with an evolving state β€” a functional pattern that maps cleanly from OCaml's `List.fold_left`.

What This Unlocks

Key Differences

ConceptOCamlRust
Event type`type kind = Start \End``enum EventType { Start, End }`
Multi-key sort`List.sort (fun a b -> ...)` with nested compare`sort_by` with `.then_with()` chaining
Active counterMutable `ref int``let mut active = 0usize` β€” direct
Fold over events`List.fold_left handler state events``events.iter().fold(initial_state, handler)`
Float comparison`compare` (total order, handles NaN poorly)`partial_cmp(...).unwrap()` β€” explicit NaN panic
/// Sweep Line Algorithm with Event Queue.
///
/// Processes interval START/END events in sorted order.
/// Applications: max overlap depth, union length, scheduling.

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum EventKind {
    End = 0,   // Process END before START at same x (avoid double-counting touches)
    Start = 1,
}

#[derive(Debug, Clone, Copy)]
struct Event {
    x: f64,
    kind: EventKind,
}

fn make_events(intervals: &[(f64, f64)]) -> Vec<Event> {
    let mut events: Vec<Event> = intervals
        .iter()
        .flat_map(|&(lo, hi)| [
            Event { x: lo, kind: EventKind::Start },
            Event { x: hi, kind: EventKind::End },
        ])
        .collect();
    // Sort by x; at same x, END before START
    events.sort_by(|a, b| {
        a.x.partial_cmp(&b.x).unwrap()
            .then(a.kind.cmp(&b.kind))
    });
    events
}

/// Maximum number of intervals active simultaneously.
pub fn max_overlap(intervals: &[(f64, f64)]) -> usize {
    let events = make_events(intervals);
    let mut active = 0i64;
    let mut best = 0usize;
    for ev in &events {
        match ev.kind {
            EventKind::Start => {
                active += 1;
                best = best.max(active as usize);
            }
            EventKind::End => active -= 1,
        }
    }
    best
}

/// Total length covered by the union of intervals.
pub fn union_length(intervals: &[(f64, f64)]) -> f64 {
    let events = make_events(intervals);
    let mut active = 0i64;
    let mut total = 0.0f64;
    let mut prev_x = 0.0f64;

    for ev in &events {
        if active > 0 {
            total += ev.x - prev_x;
        }
        prev_x = ev.x;
        match ev.kind {
            EventKind::Start => active += 1,
            EventKind::End => active -= 1,
        }
    }
    total
}

/// Find all intervals that contain point x (sweep-line approach).
pub fn intervals_at(x: f64, intervals: &[(f64, f64)]) -> Vec<usize> {
    intervals.iter()
        .enumerate()
        .filter(|(_, &(lo, hi))| lo <= x && x <= hi)
        .map(|(i, _)| i)
        .collect()
}

fn main() {
    let ivs: &[(f64, f64)] = &[(1.0, 4.0), (2.0, 6.0), (3.0, 5.0), (7.0, 9.0)];
    println!("Intervals: [1,4] [2,6] [3,5] [7,9]");
    println!("Max overlap:  {} (expected 3, at [3,4])", max_overlap(ivs));
    println!("Union length: {:.1} (expected 7.0: [1,6]βˆͺ[7,9])", union_length(ivs));

    let touching: &[(f64, f64)] = &[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)];
    println!("\nTouching intervals [0,1] [1,2] [2,3]:");
    println!("Max overlap:  {} (expected 1)", max_overlap(touching));
    println!("Union length: {:.1} (expected 3.0)", union_length(touching));

    // Meeting-room scheduling: how many rooms do we need?
    let meetings: &[(f64, f64)] = &[(0.0, 30.0), (5.0, 10.0), (15.0, 20.0)];
    println!("\nMeetings [0,30] [5,10] [15,20]: need {} rooms", max_overlap(meetings));
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_max_overlap() {
        assert_eq!(max_overlap(&[(1.0, 4.0), (2.0, 6.0), (3.0, 5.0)]), 3);
    }

    #[test]
    fn test_no_overlap() {
        assert_eq!(max_overlap(&[(1.0, 2.0), (3.0, 4.0), (5.0, 6.0)]), 1);
    }

    #[test]
    fn test_touching_not_overlapping() {
        // Touching intervals [0,1] [1,2] [2,3] β€” at each point only 1 active
        assert_eq!(max_overlap(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]), 1);
    }

    #[test]
    fn test_union_length_basic() {
        let ivs: &[(f64, f64)] = &[(1.0, 4.0), (2.0, 6.0), (3.0, 5.0), (7.0, 9.0)];
        let u = union_length(ivs);
        assert!((u - 7.0).abs() < 1e-9, "union={u}");
    }

    #[test]
    fn test_union_length_disjoint() {
        let ivs: &[(f64, f64)] = &[(0.0, 1.0), (2.0, 3.0), (4.0, 5.0)];
        assert!((union_length(ivs) - 3.0).abs() < 1e-9);
    }

    #[test]
    fn test_union_length_nested() {
        // [0,10] contains [2,5] β€” union is 10
        assert!((union_length(&[(0.0, 10.0), (2.0, 5.0)]) - 10.0).abs() < 1e-9);
    }

    #[test]
    fn test_touching_union() {
        // [0,1] [1,2] [2,3] β€” union is 3
        assert!((union_length(&[(0.0, 1.0), (1.0, 2.0), (2.0, 3.0)]) - 3.0).abs() < 1e-9);
    }

    #[test]
    fn test_meeting_rooms() {
        // [0,30] [5,10] [15,20]: max 2 overlapping at any point
        assert_eq!(max_overlap(&[(0.0, 30.0), (5.0, 10.0), (15.0, 20.0)]), 2);
    }
}
(* Sweep Line Algorithm in OCaml *)

type interval = { lo: float; hi: float }

(* Event type: 0 = END (process before START at same x), 1 = START *)
type event = { x: float; kind: int; (* 0=end, 1=start *) }

let make_events (intervals : interval list) : event list =
  List.concat_map (fun iv ->
    [{ x = iv.lo; kind = 1 }; { x = iv.hi; kind = 0 }]
  ) intervals
  |> List.sort (fun a b ->
    let cx = compare a.x b.x in
    if cx <> 0 then cx else compare a.kind b.kind)  (* END before START *)

(* Maximum overlap depth: max # of intervals active at any point *)
let max_overlap (intervals : interval list) : int =
  let events = make_events intervals in
  let active = ref 0 and best = ref 0 in
  List.iter (fun ev ->
    if ev.kind = 1 then begin incr active; if !active > !best then best := !active end
    else decr active
  ) events;
  !best

(* Total length of union of intervals *)
let union_length (intervals : interval list) : float =
  let events = make_events intervals in
  let active = ref 0 and total = ref 0.0 and prev_x = ref 0.0 in
  List.iter (fun ev ->
    if !active > 0 then total := !total +. (ev.x -. !prev_x);
    prev_x := ev.x;
    if ev.kind = 1 then incr active else decr active
  ) events;
  !total

(* All points with maximum overlap β€” just return max value here *)
let () =
  let ivs = [
    {lo=1.0; hi=4.0};
    {lo=2.0; hi=6.0};
    {lo=3.0; hi=5.0};
    {lo=7.0; hi=9.0};
  ] in
  Printf.printf "Intervals: [1,4] [2,6] [3,5] [7,9]\n";
  Printf.printf "Max overlap depth: %d  (expected 3 at [3,4])\n" (max_overlap ivs);
  Printf.printf "Union length: %.1f  (expected 7.0: [1,6]βˆͺ[7,9])\n" (union_length ivs);

  (* Touching intervals β€” should not count as overlapping *)
  let touching = [{lo=0.0;hi=1.0}; {lo=1.0;hi=2.0}; {lo=2.0;hi=3.0}] in
  Printf.printf "\nTouching intervals [0,1] [1,2] [2,3]:\n";
  Printf.printf "Max overlap: %d  (expected 1)\n" (max_overlap touching);
  Printf.printf "Union length: %.1f  (expected 3.0)\n" (union_length touching)