Line Sweep Scanline

The line sweep algorithm processes events in sorted order, typically by position or time, maintaining an active state that updates as the sweep line moves. Commonly used for interval problems, geometric algorithms, and event-driven simulations.

1. Interval Merging Sweep Line

Approach Strategy Key Steps Complexity
Sort + Merge Sort by start time, merge overlaps Compare current.end with next.start O(n log n)
Event-based Sweep Create start/end events, process sorted Track active intervals count O(n log n)
Interval Tree Augmented BST for dynamic queries Store max endpoint in each node O(n log n + k)

Example: Merge Overlapping Intervals

function mergeIntervals(intervals) {
  if (!intervals.length) return [];
  
  // Sort by start time
  intervals.sort((a, b) => a[0] - b[0]);
  
  const merged = [intervals[0]];
  
  for (let i = 1; i < intervals.length; i++) {
    const last = merged[merged.length - 1];
    const current = intervals[i];
    
    // Check for overlap
    if (current[0] <= last[1]) {
      // Merge by extending the end time
      last[1] = Math.max(last[1], current[1]);
    } else {
      // No overlap, add new interval
      merged.push(current);
    }
  }
  
  return merged;
}

console.log(mergeIntervals([[1,3], [2,6], [8,10], [15,18]]));
// [[1,6], [8,10], [15,18]]

console.log(mergeIntervals([[1,4], [4,5]]));
// [[1,5]]

// Variation: Merge with gap tolerance
function mergeWithGap(intervals, gap) {
  if (!intervals.length) return [];
  
  intervals.sort((a, b) => a[0] - b[0]);
  const merged = [intervals[0]];
  
  for (let i = 1; i < intervals.length; i++) {
    const last = merged[merged.length - 1];
    const current = intervals[i];
    
    // Merge if gap is within tolerance
    if (current[0] - last[1] <= gap) {
      last[1] = Math.max(last[1], current[1]);
    } else {
      merged.push(current);
    }
  }
  
  return merged;
}

console.log(mergeWithGap([[1,2], [4,5], [7,9]], 1));
// [[1,5], [7,9]] - gap of 1 allows [1,2] and [4,5] to merge

Example: Insert Interval

function insertInterval(intervals, newInterval) {
  const result = [];
  let i = 0;
  
  // Add all intervals before the new interval
  while (i < intervals.length && intervals[i][1] < newInterval[0]) {
    result.push(intervals[i]);
    i++;
  }
  
  // Merge overlapping intervals with new interval
  while (i < intervals.length && intervals[i][0] <= newInterval[1]) {
    newInterval[0] = Math.min(newInterval[0], intervals[i][0]);
    newInterval[1] = Math.max(newInterval[1], intervals[i][1]);
    i++;
  }
  result.push(newInterval);
  
  // Add remaining intervals
  while (i < intervals.length) {
    result.push(intervals[i]);
    i++;
  }
  
  return result;
}

console.log(insertInterval([[1,3], [6,9]], [2,5]));
// [[1,5], [6,9]]

console.log(insertInterval([[1,2], [3,5], [6,7], [8,10], [12,16]], [4,8]));
// [[1,2], [3,10], [12,16]]
Note: When merging intervals, always sort by start time first. Overlap exists when current.start <= previous.end. Merged end is max(previous.end, current.end).

2. Meeting Rooms Multiple Events

Problem Type Approach Key Insight Complexity
Can Attend All Sort and check adjacent overlaps No overlap if sorted intervals don't touch O(n log n)
Min Rooms Needed Sweep line with start/end events Max concurrent events = min rooms O(n log n)
Free Time Intervals Merge all busy times, find gaps Gaps between merged intervals O(n log n)

Example: Meeting Rooms I - Can Attend All

function canAttendMeetings(intervals) {
  if (!intervals.length) return true;
  
  // Sort by start time
  intervals.sort((a, b) => a[0] - b[0]);
  
  // Check for any overlap
  for (let i = 1; i < intervals.length; i++) {
    if (intervals[i][0] < intervals[i - 1][1]) {
      return false; // Overlap detected
    }
  }
  
  return true;
}

console.log(canAttendMeetings([[0,30], [5,10], [15,20]])); // false
console.log(canAttendMeetings([[7,10], [2,4]])); // true

Example: Meeting Rooms II - Minimum Rooms

function minMeetingRooms(intervals) {
  if (!intervals.length) return 0;
  
  // Create separate arrays for starts and ends
  const starts = intervals.map(i => i[0]).sort((a, b) => a - b);
  const ends = intervals.map(i => i[1]).sort((a, b) => a - b);
  
  let rooms = 0;
  let maxRooms = 0;
  let startPtr = 0, endPtr = 0;
  
  // Sweep through all events
  while (startPtr < starts.length) {
    if (starts[startPtr] < ends[endPtr]) {
      // Meeting starts, need a room
      rooms++;
      maxRooms = Math.max(maxRooms, rooms);
      startPtr++;
    } else {
      // Meeting ends, free a room
      rooms--;
      endPtr++;
    }
  }
  
  return maxRooms;
}

console.log(minMeetingRooms([[0,30], [5,10], [15,20]])); // 2
console.log(minMeetingRooms([[7,10], [2,4]])); // 1

// Alternative: Using events with priority
function minMeetingRoomsEvents(intervals) {
  const events = [];
  
  for (const [start, end] of intervals) {
    events.push([start, 1]);  // Start event
    events.push([end, -1]);   // End event
  }
  
  // Sort by time, end events before start events at same time
  events.sort((a, b) => a[0] === b[0] ? a[1] - b[1] : a[0] - b[0]);
  
  let rooms = 0;
  let maxRooms = 0;
  
  for (const [time, delta] of events) {
    rooms += delta;
    maxRooms = Math.max(maxRooms, rooms);
  }
  
  return maxRooms;
}

console.log(minMeetingRoomsEvents([[0,30], [5,10], [15,20]])); // 2

Example: Employee Free Time

// Input: list of employee schedules (each is a list of intervals)
function employeeFreeTime(schedules) {
  // Flatten all intervals
  const allIntervals = [];
  for (const schedule of schedules) {
    allIntervals.push(...schedule);
  }
  
  // Sort by start time
  allIntervals.sort((a, b) => a[0] - b[0]);
  
  // Merge all busy intervals
  const merged = [allIntervals[0]];
  for (let i = 1; i < allIntervals.length; i++) {
    const last = merged[merged.length - 1];
    const current = allIntervals[i];
    
    if (current[0] <= last[1]) {
      last[1] = Math.max(last[1], current[1]);
    } else {
      merged.push(current);
    }
  }
  
  // Find gaps between merged intervals
  const freeTime = [];
  for (let i = 1; i < merged.length; i++) {
    freeTime.push([merged[i - 1][1], merged[i][0]]);
  }
  
  return freeTime;
}

console.log(employeeFreeTime([
  [[1,3], [6,7]],
  [[2,4]],
  [[2,5], [9,12]]
]));
// [[5,6], [7,9]]

// Explanation:
// Employee 1: [1,3], [6,7]
// Employee 2: [2,4]
// Employee 3: [2,5], [9,12]
// Merged busy: [1,5], [6,7], [9,12]
// Free time: [5,6], [7,9]
Note: For minimum meeting rooms, the key insight is: max concurrent meetings equals the number of rooms needed. Use two-pointer sweep with sorted start/end times or event-based approach.

3. Skyline Problem Event Points

Component Representation Processing Complexity
Critical Points Building start/end x-coordinates Track height changes at each x O(n)
Active Heights Multiset/heap of current building heights Add on start, remove on end O(log n) per event
Skyline Output [x, height] when max height changes Compare previous max with current max O(n log n) total

Example: Skyline Problem

function getSkyline(buildings) {
  const events = [];
  
  // Create events: [x, isStart, height]
  for (const [left, right, height] of buildings) {
    events.push([left, true, height]);   // Start event
    events.push([right, false, height]); // End event
  }
  
  // Sort events
  // - By x coordinate
  // - Start events before end events at same x
  // - Start events: taller buildings first
  // - End events: shorter buildings first
  events.sort((a, b) => {
    if (a[0] !== b[0]) return a[0] - b[0];
    if (a[1] !== b[1]) return a[1] ? -1 : 1;
    return a[1] ? b[2] - a[2] : a[2] - b[2];
  });
  
  const result = [];
  const heights = new Map(); // height -> count
  heights.set(0, 1); // Ground level
  
  for (const [x, isStart, height] of events) {
    if (isStart) {
      heights.set(height, (heights.get(height) || 0) + 1);
    } else {
      const count = heights.get(height);
      if (count === 1) {
        heights.delete(height);
      } else {
        heights.set(height, count - 1);
      }
    }
    
    // Get current max height
    const maxHeight = Math.max(...heights.keys());
    
    // Add to result if height changed
    if (!result.length || result[result.length - 1][1] !== maxHeight) {
      result.push([x, maxHeight]);
    }
  }
  
  return result;
}

console.log(getSkyline([[2,9,10], [3,7,15], [5,12,12], [15,20,10], [19,24,8]]));
// [[2,10], [3,15], [7,12], [12,0], [15,10], [20,8], [24,0]]

// Optimized version using priority queue
class MaxHeap {
  constructor() {
    this.heap = [];
  }
  
  push(val) {
    this.heap.push(val);
    this.bubbleUp(this.heap.length - 1);
  }
  
  remove(val) {
    const idx = this.heap.indexOf(val);
    if (idx === -1) return;
    if (idx === this.heap.length - 1) {
      this.heap.pop();
      return;
    }
    this.heap[idx] = this.heap.pop();
    this.bubbleDown(idx);
  }
  
  peek() {
    return this.heap[0] || 0;
  }
  
  bubbleUp(i) {
    while (i > 0) {
      const p = Math.floor((i - 1) / 2);
      if (this.heap[p] >= this.heap[i]) break;
      [this.heap[p], this.heap[i]] = [this.heap[i], this.heap[p]];
      i = p;
    }
  }
  
  bubbleDown(i) {
    while (true) {
      let max = i;
      const left = 2 * i + 1, right = 2 * i + 2;
      if (left < this.heap.length && this.heap[left] > this.heap[max]) max = left;
      if (right < this.heap.length && this.heap[right] > this.heap[max]) max = right;
      if (max === i) break;
      [this.heap[i], this.heap[max]] = [this.heap[max], this.heap[i]];
      i = max;
    }
  }
}

function getSkylineHeap(buildings) {
  const events = [];
  
  for (const [left, right, height] of buildings) {
    events.push([left, 'start', height]);
    events.push([right, 'end', height]);
  }
  
  events.sort((a, b) => {
    if (a[0] !== b[0]) return a[0] - b[0];
    const order = { start: 0, end: 1 };
    if (a[1] !== b[1]) return order[a[1]] - order[b[1]];
    return a[1] === 'start' ? b[2] - a[2] : a[2] - b[2];
  });
  
  const result = [];
  const heap = new MaxHeap();
  heap.push(0);
  
  for (const [x, type, height] of events) {
    const prevMax = heap.peek();
    
    if (type === 'start') {
      heap.push(height);
    } else {
      heap.remove(height);
    }
    
    const currMax = heap.peek();
    if (currMax !== prevMax) {
      result.push([x, currMax]);
    }
  }
  
  return result;
}
Note: The skyline problem requires careful event sorting: start events before end events at same x, and for start events, process taller buildings first to avoid duplicate key points.

4. Rectangle Overlap Detection

Problem Type Approach Key Insight Complexity
Two Rectangles Check boundary conditions Overlap if NOT separated in any axis O(1)
N Rectangles (Pairs) All pairs comparison Check each pair for overlap O(n²)
Area Union (Sweep Line) Vertical sweep with active segments Process x-events, merge y-intervals O(n² log n)
Interval Tree 2D Separate x and y interval trees Query overlapping intervals in both dims O(n log n + k)

Example: Rectangle Overlap (2 Rectangles)

// Rectangle represented as [x1, y1, x2, y2] (bottom-left, top-right)
function isRectangleOverlap(rec1, rec2) {
  const [x1, y1, x2, y2] = rec1;
  const [x3, y3, x4, y4] = rec2;
  
  // Check if NOT overlapping (separated)
  // Separated if: rec1 is left of rec2 OR rec1 is right of rec2
  //            OR rec1 is below rec2 OR rec1 is above rec2
  const separated = x2 <= x3 || x4 <= x1 || y2 <= y3 || y4 <= y1;
  
  return !separated;
}

console.log(isRectangleOverlap([0,0,2,2], [1,1,3,3])); // true
console.log(isRectangleOverlap([0,0,1,1], [1,0,2,1])); // false (touching but not overlapping)
console.log(isRectangleOverlap([0,0,1,1], [2,2,3,3])); // false

// Alternative: Check overlap directly
function isRectangleOverlapDirect(rec1, rec2) {
  const [x1, y1, x2, y2] = rec1;
  const [x3, y3, x4, y4] = rec2;
  
  // Overlap in both x and y dimensions
  const xOverlap = Math.max(x1, x3) < Math.min(x2, x4);
  const yOverlap = Math.max(y1, y3) < Math.min(y2, y4);
  
  return xOverlap && yOverlap;
}

Example: Rectangle Area (Union of Two Rectangles)

function computeArea(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) {
  // Calculate individual areas
  const area1 = (ax2 - ax1) * (ay2 - ay1);
  const area2 = (bx2 - bx1) * (by2 - by1);
  
  // Calculate overlap area
  const overlapWidth = Math.max(0, Math.min(ax2, bx2) - Math.max(ax1, bx1));
  const overlapHeight = Math.max(0, Math.min(ay2, by2) - Math.max(ay1, by1));
  const overlapArea = overlapWidth * overlapHeight;
  
  // Total area = sum of areas - overlap
  return area1 + area2 - overlapArea;
}

console.log(computeArea(-3, 0, 3, 4, 0, -1, 9, 2)); // 45
// Rectangle 1: 6 * 4 = 24
// Rectangle 2: 9 * 3 = 27
// Overlap: 3 * 2 = 6
// Total: 24 + 27 - 6 = 45

Example: Rectangle Area II (Union of Multiple Rectangles)

function rectangleArea(rectangles) {
  const MOD = 1e9 + 7;
  
  // Collect all unique x and y coordinates
  const xCoords = new Set();
  const yCoords = new Set();
  
  for (const [x1, y1, x2, y2] of rectangles) {
    xCoords.add(x1);
    xCoords.add(x2);
    yCoords.add(y1);
    yCoords.add(y2);
  }
  
  const xs = Array.from(xCoords).sort((a, b) => a - b);
  const ys = Array.from(yCoords).sort((a, b) => a - b);
  
  let totalArea = 0;
  
  // Coordinate compression: check each grid cell
  for (let i = 0; i < xs.length - 1; i++) {
    for (let j = 0; j < ys.length - 1; j++) {
      const x1 = xs[i], x2 = xs[i + 1];
      const y1 = ys[j], y2 = ys[j + 1];
      
      // Check if this cell is covered by any rectangle
      let covered = false;
      for (const [rx1, ry1, rx2, ry2] of rectangles) {
        if (rx1 <= x1 && x2 <= rx2 && ry1 <= y1 && y2 <= ry2) {
          covered = true;
          break;
        }
      }
      
      if (covered) {
        const area = (x2 - x1) * (y2 - y1);
        totalArea = (totalArea + area) % MOD;
      }
    }
  }
  
  return totalArea;
}

console.log(rectangleArea([[0,0,2,2], [1,0,2,3], [1,0,3,1]])); // 6

// Optimized sweep line approach
function rectangleAreaSweepLine(rectangles) {
  const MOD = 1e9 + 7;
  const events = [];
  
  // Create events for each rectangle
  for (const [x1, y1, x2, y2] of rectangles) {
    events.push([x1, y1, y2, 1]);  // Start of rectangle
    events.push([x2, y1, y2, -1]); // End of rectangle
  }
  
  // Sort by x coordinate
  events.sort((a, b) => a[0] - b[0]);
  
  let totalArea = 0;
  let prevX = 0;
  const active = []; // [y1, y2, count]
  
  for (const [x, y1, y2, type] of events) {
    // Calculate area for previous x range
    if (active.length > 0) {
      const yLength = getTotalYLength(active);
      totalArea = (totalArea + (x - prevX) * yLength) % MOD;
    }
    
    // Update active intervals
    if (type === 1) {
      active.push([y1, y2]);
    } else {
      const idx = active.findIndex(([a, b]) => a === y1 && b === y2);
      active.splice(idx, 1);
    }
    
    prevX = x;
  }
  
  return totalArea;
}

function getTotalYLength(intervals) {
  if (!intervals.length) return 0;
  
  // Sort and merge intervals
  const sorted = intervals.slice().sort((a, b) => a[0] - b[0]);
  const merged = [sorted[0]];
  
  for (let i = 1; i < sorted.length; i++) {
    const last = merged[merged.length - 1];
    const curr = sorted[i];
    
    if (curr[0] <= last[1]) {
      last[1] = Math.max(last[1], curr[1]);
    } else {
      merged.push(curr);
    }
  }
  
  return merged.reduce((sum, [y1, y2]) => sum + (y2 - y1), 0);
}
Note: Rectangle overlap: two rectangles overlap if they are NOT separated in both x and y dimensions. For union area of multiple rectangles, use coordinate compression or sweep line with interval merging.

5. Event-driven Processing Pattern

Pattern Event Types Processing Order Use Case
Timeline Events Start/End events with timestamps Sort by time, process sequentially Meeting rooms, resource allocation
Priority Events Events with importance/urgency Sort by priority, then time Task scheduling, CPU scheduling
Geometric Events Points, lines, rectangles Sweep line in spatial order Skyline, closest pair, intersections
State Transition Events that change system state Maintain active state, update on events Finite state machines, simulations

Example: Task Scheduler with Cooldown

// Given tasks and cooldown n, find minimum intervals needed
function leastInterval(tasks, n) {
  // Count frequency of each task
  const freq = new Map();
  for (const task of tasks) {
    freq.set(task, (freq.get(task) || 0) + 1);
  }
  
  // Get max frequency
  const maxFreq = Math.max(...freq.values());
  
  // Count how many tasks have max frequency
  const maxCount = Array.from(freq.values()).filter(f => f === maxFreq).length;
  
  // Calculate minimum intervals
  // Pattern: A _ _ A _ _ A (for n=2, freq=3)
  // Intervals = (maxFreq - 1) * (n + 1) + maxCount
  const minIntervals = (maxFreq - 1) * (n + 1) + maxCount;
  
  // Can't be less than total tasks
  return Math.max(minIntervals, tasks.length);
}

console.log(leastInterval(['A','A','A','B','B','B'], 2)); // 8
// A -> B -> idle -> A -> B -> idle -> A -> B

console.log(leastInterval(['A','A','A','B','B','B'], 0)); // 6
// A -> B -> A -> B -> A -> B

console.log(leastInterval(['A','A','A','A','A','A','B','C','D','E','F','G'], 2)); // 16
// A -> B -> C -> A -> D -> E -> A -> F -> G -> A -> idle -> idle -> A -> idle -> idle -> A

Example: Car Fleet (Event-driven)

function carFleet(target, position, speed) {
  const n = position.length;
  if (n === 0) return 0;
  
  // Create cars with their arrival times
  const cars = position.map((pos, i) => ({
    pos,
    time: (target - pos) / speed[i]
  }));
  
  // Sort by starting position (descending - closest to target first)
  cars.sort((a, b) => b.pos - a.pos);
  
  let fleets = 0;
  let slowestTime = 0;
  
  // Process cars from closest to target
  for (const car of cars) {
    // If this car takes longer, it forms a new fleet
    if (car.time > slowestTime) {
      fleets++;
      slowestTime = car.time;
    }
    // Otherwise, it catches up to the previous fleet
  }
  
  return fleets;
}

console.log(carFleet(12, [10,8,0,5,3], [2,4,1,1,3])); // 3

// Explanation:
// Car at position 10, speed 2: time = (12-10)/2 = 1
// Car at position 8, speed 4: time = (12-8)/4 = 1
// Car at position 5, speed 1: time = (12-5)/1 = 7
// Car at position 3, speed 3: time = (12-3)/3 = 3
// Car at position 0, speed 1: time = (12-0)/1 = 12
//
// Sorted by position: [10,8,5,3,0]
// Times: [1,1,7,3,12]
// Fleet 1: Cars at 10,8 (both arrive at time 1)
// Fleet 2: Car at 5 (arrives at time 7, catches car at 3)
// Fleet 3: Car at 0 (arrives at time 12, slowest)

Example: The Earliest Moment When Everyone Becomes Friends

// Union-Find for connectivity
class UnionFind {
  constructor(n) {
    this.parent = Array(n).fill(0).map((_, i) => i);
    this.rank = Array(n).fill(1);
    this.components = n;
  }
  
  find(x) {
    if (this.parent[x] !== x) {
      this.parent[x] = this.find(this.parent[x]); // Path compression
    }
    return this.parent[x];
  }
  
  union(x, y) {
    const rootX = this.find(x);
    const rootY = this.find(y);
    
    if (rootX === rootY) return false;
    
    // Union by rank
    if (this.rank[rootX] > this.rank[rootY]) {
      this.parent[rootY] = rootX;
    } else if (this.rank[rootX] < this.rank[rootY]) {
      this.parent[rootX] = rootY;
    } else {
      this.parent[rootY] = rootX;
      this.rank[rootX]++;
    }
    
    this.components--;
    return true;
  }
  
  isConnected() {
    return this.components === 1;
  }
}

function earliestAcq(logs, n) {
  // Sort logs by timestamp
  logs.sort((a, b) => a[0] - b[0]);
  
  const uf = new UnionFind(n);
  
  // Process events in chronological order
  for (const [timestamp, x, y] of logs) {
    uf.union(x, y);
    
    // Check if all are connected
    if (uf.isConnected()) {
      return timestamp;
    }
  }
  
  return -1; // Not everyone becomes friends
}

console.log(earliestAcq([
  [20190101,0,1],
  [20190104,3,4],
  [20190107,2,3],
  [20190211,1,5],
  [20190224,2,4],
  [20190301,0,3],
  [20190312,1,2],
  [20190322,4,5]
], 6)); // 20190301

// Explanation: At timestamp 20190301, everyone becomes connected
Note: Event-driven processing follows a pattern: sort events by time/position, maintain active state, update state on each event. Use appropriate data structures (stack, heap, union-find) based on the problem requirements.

Summary: Line Sweep Scanline Pattern

  • Core Concept: Process events in sorted order (time/position), maintain active state that updates at each event
  • Interval Merging: Sort by start time, merge when current.start ≤ previous.end, extend end to max
  • Meeting Rooms: Min rooms = max concurrent meetings, use two-pointer sweep or event-based counting
  • Skyline Problem: Create start/end events, track active heights with multiset/heap, output when max changes
  • Rectangle Overlap: Two rectangles overlap if NOT separated in both x and y axes
  • Rectangle Area Union: Coordinate compression creates grid cells, or sweep line with y-interval merging
  • Event Sorting: Critical for correctness - consider event type priority at same timestamp
  • State Management: Use appropriate data structure: stack for monotonic, heap for priority, map for frequencies
  • Complexity: Typically O(n log n) for sorting + O(n) or O(n log n) for processing events