Interval Scheduling Patterns

1. Minimum Meeting Rooms Required

Approach Method Time Space
Min Heap Track end times of ongoing meetings O(n log n) O(n)
Chronological Ordering Separate start/end events, sort and scan O(n log n) O(n)
Sweep Line Event-driven: +1 for start, -1 for end O(n log n) O(n)
Brute Force Check max overlaps at each point O(n^2) Not recommended

Example: Min heap approach

function minMeetingRooms(intervals) {
    if (intervals.length === 0) return 0;
    
    // Sort by start time
    intervals.sort((a, b) => a[0] - b[0]);
    
    // Min heap to track end times of ongoing meetings
    const minHeap = new MinPriorityQueue({ priority: x => x });
    
    minHeap.enqueue(intervals[0][1]); // First meeting's end time
    
    for (let i = 1; i < intervals.length; i++) {
        const [start, end] = intervals[i];
        
        // If earliest ending meeting finished before current starts
        if (minHeap.front().element <= start) {
            minHeap.dequeue(); // Reuse this room
        }
        
        // Add current meeting's end time
        minHeap.enqueue(end);
    }
    
    // Heap size = number of rooms needed
    return minHeap.size();
}

// Example: [[0,30],[5,10],[15,20]]
// Sort: [[0,30],[5,10],[15,20]] (already sorted)
// Process [0,30]: heap = [30]
// Process [5,10]: 30 > 5, heap = [10,30]
// Process [15,20]: 10 <= 15, reuse room, heap = [20,30]
// Result: 2 rooms

Example: Chronological ordering

function minMeetingRoomsChronological(intervals) {
    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 endPointer = 0;
    
    for (let i = 0; i < starts.length; i++) {
        // If a meeting ends before or when current starts
        if (starts[i] >= ends[endPointer]) {
            rooms--; // Free up a room
            endPointer++;
        }
        
        rooms++; // Allocate room for current meeting
    }
    
    return rooms;
}

// Example: [[0,30],[5,10],[15,20]]
// starts: [0, 5, 15]
// ends:   [10, 20, 30]
// 
// i=0, start=0: rooms=1
// i=1, start=5: 5 < 10, rooms=2
// i=2, start=15: 15 >= 10, rooms=2-1+1=2
// Result: 2 rooms

Example: Sweep line with events

function minMeetingRoomsSweep(intervals) {
    const events = [];
    
    // Create start and end events
    for (const [start, end] of intervals) {
        events.push([start, 1]);   // Meeting starts (+1)
        events.push([end, -1]);    // Meeting ends (-1)
    }
    
    // Sort events: by time, then ends before starts (tie-breaking)
    events.sort((a, b) => {
        if (a[0] !== b[0]) return a[0] - b[0];
        return a[1] - b[1]; // -1 before 1 (end before start)
    });
    
    let rooms = 0;
    let maxRooms = 0;
    
    for (const [time, delta] of events) {
        rooms += delta;
        maxRooms = Math.max(maxRooms, rooms);
    }
    
    return maxRooms;
}

// Example: [[0,30],[5,10],[15,20]]
// Events: [[0,1],[5,1],[10,-1],[15,1],[20,-1],[30,-1]]
// Process: 0→1, 5→2, 10→1, 15→2, 20→1, 30→0
// Max: 2 rooms
Note: When sorting events at the same time, end events must come before start events (-1 before +1). This ensures a meeting ending at time T frees the room before a meeting starting at time T occupies it. Without this tie-breaking, we'd incorrectly count an extra room.

2. Employee Free Time Problem

Approach Method Time Space
Merge All Intervals Flatten, sort, merge, find gaps O(n log n) O(n)
Priority Queue K-way merge of sorted lists O(n log k) O(k)
Sweep Line Event processing for busy periods O(n log n) O(n)
Two Pointers If each employee has 1 interval only O(n log n) O(1)

Example: Flatten and merge approach

function employeeFreeTime(schedule) {
    // Flatten all intervals into single array
    const intervals = [];
    for (const employee of schedule) {
        for (const interval of employee) {
            intervals.push(interval);
        }
    }
    
    // Sort by start time
    intervals.sort((a, b) => a.start - b.start);
    
    // Merge overlapping intervals
    const merged = [intervals[0]];
    
    for (let i = 1; i < intervals.length; i++) {
        const current = intervals[i];
        const last = merged[merged.length - 1];
        
        if (current.start <= last.end) {
            // Overlapping or adjacent - merge
            last.end = Math.max(last.end, current.end);
        } else {
            // Gap found - but don't add yet
            merged.push(current);
        }
    }
    
    // Find gaps between merged intervals
    const freeTime = [];
    for (let i = 1; i < merged.length; i++) {
        const gapStart = merged[i - 1].end;
        const gapEnd = merged[i].start;
        
        if (gapStart < gapEnd) {
            freeTime.push(new Interval(gapStart, gapEnd));
        }
    }
    
    return freeTime;
}

// Example: [[[1,3],[4,6]], [[2,4]], [[2,5],[9,12]]]
// Flattened: [[1,3],[4,6],[2,4],[2,5],[9,12]]
// Sorted: [[1,3],[2,4],[2,5],[4,6],[9,12]]
// Merged: [[1,6],[9,12]]
// Free time: [[6,9]]

Example: Priority queue k-way merge

function employeeFreeTimePQ(schedule) {
    // Min heap: [interval, employeeIndex, intervalIndex]
    const pq = new MinPriorityQueue({ 
        priority: x => x[0].start 
    });
    
    // Initialize with first interval of each employee
    for (let i = 0; i < schedule.length; i++) {
        if (schedule[i].length > 0) {
            pq.enqueue([schedule[i][0], i, 0]);
        }
    }
    
    const freeTime = [];
    let prevEnd = pq.front().element[0].start;
    
    while (!pq.isEmpty()) {
        const [interval, empIdx, intIdx] = pq.dequeue().element;
        
        // Found a gap
        if (prevEnd < interval.start) {
            freeTime.push(new Interval(prevEnd, interval.start));
        }
        
        prevEnd = Math.max(prevEnd, interval.end);
        
        // Add next interval from same employee
        if (intIdx + 1 < schedule[empIdx].length) {
            pq.enqueue([
                schedule[empIdx][intIdx + 1],
                empIdx,
                intIdx + 1
            ]);
        }
    }
    
    return freeTime;
}

// More efficient when k (employees) << n (total intervals)
// O(n log k) vs O(n log n)

3. Interval List Intersections

Approach Technique Time Space
Two Pointers Compare intervals from both lists O(m + n) O(1)
Intersection Logic max(start1, start2) to min(end1, end2) Overlaps if start ≤ end Simple and elegant
Advance Pointer Move pointer of interval that ends first Ensures no interval missed Critical for correctness
Multiple Lists Use priority queue for k lists O(n log k) Generalization

Example: Two pointers intersection

function intervalIntersection(firstList, secondList) {
    const result = [];
    let i = 0, j = 0;
    
    while (i < firstList.length && j < secondList.length) {
        const [start1, end1] = firstList[i];
        const [start2, end2] = secondList[j];
        
        // Calculate intersection
        const startIntersection = Math.max(start1, start2);
        const endIntersection = Math.min(end1, end2);
        
        // Valid intersection exists
        if (startIntersection <= endIntersection) {
            result.push([startIntersection, endIntersection]);
        }
        
        // Advance pointer of interval that ends first
        if (end1 < end2) {
            i++;
        } else {
            j++;
        }
    }
    
    return result;
}

// Example:
// A = [[0,2],[5,10],[13,23],[24,25]]
// B = [[1,5],[8,12],[15,24],[25,26]]
// 
// Intersections:
// [0,2] ∩ [1,5] = [1,2]
// [5,10] ∩ [1,5] = [5,5]
// [5,10] ∩ [8,12] = [8,10]
// [13,23] ∩ [15,24] = [15,23]
// [24,25] ∩ [15,24] = [24,24]
// [24,25] ∩ [25,26] = [25,25]
// Result: [[1,2],[5,5],[8,10],[15,23],[24,24],[25,25]]

Example: Visualizing intersection logic

// Understanding intersection calculation:
function hasIntersection(interval1, interval2) {
    const [start1, end1] = interval1;
    const [start2, end2] = interval2;
    
    // No intersection cases:
    // 1. interval1 completely before interval2: end1 < start2
    // 2. interval2 completely before interval1: end2 < start1
    
    // Has intersection if neither case is true:
    return !(end1 < start2 || end2 < start1);
}

function getIntersection(interval1, interval2) {
    const [start1, end1] = interval1;
    const [start2, end2] = interval2;
    
    const start = Math.max(start1, start2);
    const end = Math.min(end1, end2);
    
    // Valid if start <= end
    return start <= end ? [start, end] : null;
}

// Key insight: 
// Intersection start = max of two starts (later one)
// Intersection end = min of two ends (earlier one)
// Valid only if start <= end

// Examples:
// [1,5] ∩ [3,7] = [max(1,3), min(5,7)] = [3,5] ✓
// [1,3] ∩ [5,7] = [max(1,5), min(3,7)] = [5,3] ✗ (5 > 3)

4. Insert Interval and Merge

Step Action Condition Complexity
1. Add Before Add intervals completely before new interval interval.end < newInterval.start Direct copy
2. Merge Overlapping Merge all overlapping intervals interval.start <= newInterval.end Update boundaries
3. Add After Add remaining intervals after new interval interval.start > newInterval.end Direct copy
In-place Option Modify input array instead of creating new More complex pointer management O(1) space

Example: Insert and merge interval

function insert(intervals, newInterval) {
    const result = [];
    let i = 0;
    const n = intervals.length;
    
    // 1. Add all intervals before newInterval
    while (i < n && intervals[i][1] < newInterval[0]) {
        result.push(intervals[i]);
        i++;
    }
    
    // 2. Merge overlapping intervals
    while (i < n && 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);
    
    // 3. Add all intervals after newInterval
    while (i < n) {
        result.push(intervals[i]);
        i++;
    }
    
    return result;
}

// Example: intervals = [[1,3],[6,9]], newInterval = [2,5]
// Step 1: No intervals end before 2
// Step 2: [1,3] overlaps, merge to [1,5]
// Step 3: Add [6,9]
// Result: [[1,5],[6,9]]

// Example: intervals = [[1,2],[3,5],[6,7],[8,10],[12,16]]
//          newInterval = [4,8]
// Step 1: Add [1,2]
// Step 2: Merge [3,5],[6,7],[8,10] -> [3,10]
// Step 3: Add [12,16]
// Result: [[1,2],[3,10],[12,16]]

Example: Alternative approach with three passes

function insertAlternative(intervals, newInterval) {
    const result = [];
    let [newStart, newEnd] = newInterval;
    let inserted = false;
    
    for (const [start, end] of intervals) {
        if (end < newStart) {
            // Current interval completely before new
            result.push([start, end]);
        } else if (start > newEnd) {
            // Current interval completely after new
            if (!inserted) {
                result.push([newStart, newEnd]);
                inserted = true;
            }
            result.push([start, end]);
        } else {
            // Overlapping - expand new interval
            newStart = Math.min(newStart, start);
            newEnd = Math.max(newEnd, end);
        }
    }
    
    // If not inserted yet, add at end
    if (!inserted) {
        result.push([newStart, newEnd]);
    }
    
    return result;
}

// Single pass through intervals
// Track whether merged interval has been added

5. Task Scheduler Problem (Greedy)

Approach Strategy Formula Complexity
Greedy Max Frequency Schedule most frequent task first with cooldown max(n, (maxFreq-1)*cooldown + tiesCount) O(n)
Heap Simulation Use max heap + cooldown queue Simulate actual scheduling O(n log 26)
Greedy Formula Calculate without simulation Slots needed = max of tasks or formula O(n)
Idle Slots Count idle time needed (maxFreq-1) * cooldown - other tasks Alternative view

Example: Greedy formula approach

function leastInterval(tasks, n) {
    // Count frequency of each task
    const freq = new Array(26).fill(0);
    for (const task of tasks) {
        freq[task.charCodeAt(0) - 'A'.charCodeAt(0)]++;
    }
    
    // Find max frequency
    const maxFreq = Math.max(...freq);
    
    // Count how many tasks have max frequency
    const maxCount = freq.filter(f => f === maxFreq).length;
    
    // Calculate minimum intervals
    // Pattern: (A _ _ A _ _ A) for maxFreq=3, cooldown=2
    // Partitions: (maxFreq - 1)
    // Each partition size: (n + 1) where n is cooldown
    // Last partition: maxCount tasks
    
    const partitions = maxFreq - 1;
    const partitionSize = n + 1;
    const emptySlots = partitions * partitionSize;
    const availableSlots = emptySlots + maxCount;
    const idles = Math.max(0, availableSlots - tasks.length);
    
    return tasks.length + idles;
}

// Alternative cleaner formula:
function leastIntervalFormula(tasks, n) {
    const freq = new Array(26).fill(0);
    for (const task of tasks) {
        freq[task.charCodeAt(0) - 'A'.charCodeAt(0)]++;
    }
    
    const maxFreq = Math.max(...freq);
    const maxCount = freq.filter(f => f === maxFreq).length;
    
    // Either all tasks fit, or need idle slots
    return Math.max(
        tasks.length,
        (maxFreq - 1) * (n + 1) + maxCount
    );
}

// Example: tasks = ["A","A","A","B","B","B"], n = 2
// Frequency: A=3, B=3
// maxFreq = 3, maxCount = 2
// Formula: (3-1)*(2+1) + 2 = 2*3 + 2 = 8
// Schedule: A B _ A B _ A B
// Result: 8

Example: Heap simulation approach

function leastIntervalHeap(tasks, n) {
    // Count frequencies
    const freq = {};
    for (const task of tasks) {
        freq[task] = (freq[task] || 0) + 1;
    }
    
    // Max heap of frequencies
    const maxHeap = new MaxPriorityQueue({ priority: x => x });
    for (const count of Object.values(freq)) {
        maxHeap.enqueue(count);
    }
    
    let time = 0;
    
    while (!maxHeap.isEmpty()) {
        const temp = [];
        
        // Try to schedule n+1 tasks (or until heap empty)
        for (let i = 0; i < n + 1; i++) {
            if (!maxHeap.isEmpty()) {
                const count = maxHeap.dequeue().element;
                if (count > 1) {
                    temp.push(count - 1);
                }
            }
        }
        
        // Add remaining tasks back to heap
        for (const count of temp) {
            maxHeap.enqueue(count);
        }
        
        // If heap not empty, full cycle; else just processed tasks
        time += maxHeap.isEmpty() ? temp.length + 1 : n + 1;
    }
    
    return time;
}

// Simulates actual scheduling cycle by cycle
// Each cycle: schedule up to (n+1) different tasks
// More intuitive but slower than formula approach
Note: Task Scheduler formula derivation: With max frequency F and cooldown N, we need F-1 complete "frames" of size N+1 (to satisfy cooldown), plus the final batch of tasks with max frequency. The answer is max(tasks.length, (F-1)*(N+1) + maxCount) because if we have enough task variety, no idle time is needed.

Summary: Interval Scheduling Patterns

  • Minimum Meeting Rooms: Min heap tracking end times, or chronological ordering of starts/ends, or sweep line with events - all O(n log n)
  • Min Heap Pattern: Sort by start, maintain heap of end times, reuse room if earliest end ≤ current start, heap size = rooms needed
  • Chronological Pattern: Separate sorted start/end arrays, two pointers, increment rooms on start, decrement on end
  • Sweep Line Events: Create [time, delta] events, sort with tie-breaking (ends before starts), track running count, record max
  • Employee Free Time: Flatten all intervals, sort, merge overlapping, gaps between merged = free time - O(n log n)
  • K-way Merge: Priority queue merging k sorted lists - O(n log k) more efficient when k << n
  • Interval Intersection: Two pointers, intersection = [max(start1,start2), min(end1,end2)], valid if start ≤ end, advance pointer of interval ending first
  • Insert Interval: Three phases: add before, merge overlapping, add after - O(n) single pass
  • Merge While Inserting: Expand new interval boundaries while overlapping, track insertion flag to avoid duplicates
  • Task Scheduler Greedy: Formula: max(tasks.length, (maxFreq-1)*(cooldown+1) + maxCount) - O(n) time
  • Scheduler Intuition: Most frequent task creates frames, other tasks fill frames, maxCount tasks with same max frequency go together in final frame
  • Heap Simulation: Max heap of frequencies, each cycle schedule up to cooldown+1 tasks, decrement and re-add - more intuitive but slower
  • Common Pattern: Sort intervals, use greedy or data structure (heap/events), handle overlaps, O(n log n) typical complexity