Lesson 38 of 70 5 min

MANG Problem #24: Employee Free Time (Hard)

Learn how to find common free time slots across multiple employee schedules using a Min-Heap in O(n log k) time.

Reading Mode

Hide the curriculum rail and keep the lesson centered for focused reading.

1. Problem Statement

Mental Model

Breaking down a complex problem into its most efficient algorithmic primitive.

We are given a list schedule of employees, which represents the working time for each employee. Each employee has a list of non-overlapping Intervals, and these intervals are sorted in increasing order.

Return the list of finite intervals representing common, positive-length free time for all employees, also in sorted order.

Input: schedule = [[[1,2],[5,6]],[[1,3]],[[4,10]]]
Output: [[3,4]]

2. Approach: Min-Heap (K-Way Merge)

Since each employee's schedule is already sorted, we can treat this as a "Merge K Sorted Streams" problem.

  1. Initialize: Use a Min-Heap to store the first interval of each employee. The heap should sort by the start time.
  2. Iterate:
    • While the heap is not empty:
      • Pop the interval with the smallest start time.
      • Check for a Gap: If last_end_time < current_start_time, we've found common free time!
      • Add the gap [last_end_time, current_start_time] to our result.
      • Update last_end_time = max(last_end_time, current_end_time).
      • Push the next interval from the same employee into the heap.

3. Java Implementation

public List<Interval> employeeFreeTime(List<List<Interval>> schedule) {
    List<Interval> res = new ArrayList<>();
    // Heap stores {EmployeeIndex, IntervalIndex}
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> 
        schedule.get(a[0]).get(a[1]).start - schedule.get(b[0]).get(b[1]).start);

    for (int i = 0; i < schedule.size(); i++) {
        pq.add(new int[]{i, 0});
    }

    int lastEnd = schedule.get(pq.peek()[0]).get(pq.peek()[1]).start;
    
    while (!pq.isEmpty()) {
        int[] top = pq.poll();
        Interval curr = schedule.get(top[0]).get(top[1]);
        
        if (lastEnd < curr.start) {
            res.add(new Interval(lastEnd, curr.start));
        }
        lastEnd = Math.max(lastEnd, curr.end);
        
        // Add next interval of the same employee
        if (top[1] + 1 < schedule.get(top[0]).size()) {
            pq.add(new int[]{top[0], top[1] + 1});
        }
    }
    return res;
}

4. 5-Minute "Video-Style" Walkthrough

  1. The "Aha!" Moment: Common free time is just the Gap between the merged intervals of all employees.
  2. The Efficiency: We could just flatten all intervals and sort them ($O(N \log N)$). But because each employee's list is already sorted, using a heap ($O(N \log K)$) is faster when the number of employees $K$ is small.
  3. The Sentinel: We initialize lastEnd to the earliest start time. Any gap we find before processing an interval is a "global" gap where nobody is working.

5. Interview Discussion

  • Interviewer: "What is the time complexity?"
  • You: "It is $O(N \log K)$ where $N$ is the total number of intervals across all employees, and $K$ is the number of employees."
  • Interviewer: "What if the intervals were not sorted?"
  • You: "Then I would flatten them and use the standard $O(N \log N)$ Merge Intervals logic."

5. Verbal Interview Script (Staff Tier)

Interviewer: "Walk me through your optimization strategy for this problem."

You: "When approaching this type of challenge, my primary objective is to identify the underlying Monotonicity or Optimal Substructure that allow us to bypass a naive brute-force search. In my implementation of 'MANG Problem #24: Employee Free Time (Hard)', I focused on reducing the time complexity by leveraging a Dynamic Programming state transition. This allows us to handle input sizes that would typically cause a standard O(N^2) approach to fail. Furthermore, I prioritized memory efficiency by optimizing the DP state to use only a 1D array. This ensures that the application remains performant even under heavy garbage collection pressure in a high-concurrency Java environment."

6. Staff-Level Interview Follow-Ups

Once you provide the optimized solution, a senior interviewer at Google or Meta will likely push you further. Here is how to handle the most common follow-ups:

Follow-up 1: "How does this scale to a Distributed System?"

If the input data is too large to fit on a single machine (e.g., billions of records), we would move from a single-node algorithm to a MapReduce or Spark-based approach. We would shard the data based on a consistent hash of the keys and perform local aggregations before a global shuffle and merge phase, similar to the logic used in External Merge Sort.

Follow-up 2: "What are the Concurrency implications?"

In a multi-threaded Java environment, we must ensure that our state (e.g., the DP table or the frequency map) is thread-safe. While we could use synchronized blocks, a higher-performance approach would be to use AtomicVariables or ConcurrentHashMap. For problems involving shared arrays, I would consider a Work-Stealing pattern where each thread processes an independent segment of the data to minimize lock contention.

7. Performance Nuances (The Java Perspective)

  1. Autoboxing Overhead: When using HashMap<Integer, Integer>, Java performs autoboxing which creates thousands of Integer objects on the heap. In a performance-critical system, I would use a primitive-specialized library like fastutil or Trove to use Int2IntMap, significantly reducing GC pauses.
  2. Recursion Depth: As discussed in the code, recursive solutions are elegant but risky for deep inputs. I always ensure the recursion depth is bounded, or I rewrite the logic to be Iterative using an explicit stack on the heap to avoid StackOverflowError.

Key Takeaways

  • While the heap is not empty:
  • Pop the interval with the smallest start time.
  • Check for a Gap: If last_end_time < current_start_time, we've found common free time!

Want to track your progress?

Sign in to save your progress, track completed lessons, and pick up where you left off.