1834. Single Threaded CPU - Explanation

Problem Link

Description

You are given n tasks labeled from 0 to n - 1 represented by a 2D integer array tasks, where tasks[i] = [enqueueTimei, processingTimei] means that the ith task will be available to process at enqueueTime[i] and will take processingTime[i] to finish processing.

You have a single-threaded CPU that can process at most one task at a time and will act in the following way:

  • If the CPU is idle and there are no available tasks to process, the CPU remains idle.

  • If the CPU is idle and there are available tasks, the CPU will choose the one with the shortest processing time. If multiple tasks have the same shortest processing time, it will choose the task with the smallest index.

  • Once a task is started to process, the CPU will process the entire task without stopping.

  • The CPU can finish a task then start a new one instantly.

Return the order in which the CPU will process the tasks.

Example 1:

Input: tasks = [[1,4],[3,3],[2,1]]

Output: [0,2,1]

Example 2:

Input: tasks = [[5,2],[4,4],[4,1],[2,1],[3,3]]

Output: [3,4,2,0,1]

Constraints:

  • 1 <= tasks.length <= 100,000
  • 1 <= enqueueTime[i], processingTime[i] <= 1,000,000,000


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Min-Heap (Priority Queue) - Essential for efficiently selecting the task with minimum processing time among available tasks
  • Sorting - Tasks must be processed in order of enqueue time, requiring initial sorting or a heap
  • Simulation - Understanding how to model a single-threaded CPU processing tasks over time
  • Index Tracking - Preserving original task indices after sorting to return the correct execution order

1. Two Min-Heaps

Intuition

A single-threaded CPU processes one task at a time. At any moment, we need to know which tasks are available (enqueue time has passed) and pick the one with the shortest processing time. A min-heap efficiently gives us the task with minimum processing time among available tasks. We use two heaps: one for pending tasks (sorted by enqueue time) and one for available tasks (sorted by processing time, then by index).

Algorithm

  1. Store all tasks with their original indices in a min-heap ordered by enqueue time.
  2. Initialize the current time and an empty result list.
  3. While there are pending or available tasks:
    • Move all tasks from the pending heap to the available heap if their enqueue time has been reached.
    • If no tasks are available, jump the current time to the next pending task's enqueue time.
    • Otherwise, pop the task with the shortest processing time from the available heap, add its index to the result, and advance time by its processing duration.
  4. Return the result list.
class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        available = []
        pending = []
        for i, (enqueueTime, processTime) in enumerate(tasks):
            heapq.heappush(pending, (enqueueTime, processTime, i))

        time = 0
        res = []
        while pending or available:
            while pending and pending[0][0] <= time:
                enqueueTime, processTime, i = heapq.heappop(pending)
                heapq.heappush(available, (processTime, i))

            if not available:
                time = pending[0][0]
                continue

            processTime, i = heapq.heappop(available)
            time += processTime
            res.append(i)

        return res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

2. Sorting + Min-Heap

Intuition

Instead of using two heaps, we can sort the tasks by enqueue time first. This allows us to iterate through tasks in order and add them to the available heap as they become ready. We only need one heap for available tasks, simplifying the implementation while maintaining the same logic.

Algorithm

  1. Attach the original index to each task and sort all tasks by enqueue time.
  2. Initialize the current time to the first task's enqueue time and an empty min-heap for available tasks.
  3. Use an index i to track which tasks have been considered.
  4. While tasks remain or the heap is not empty:
    • Add all tasks with enqueue time at or before the current time to the heap.
    • If the heap is empty, jump time to the next task's enqueue time.
    • Otherwise, pop the task with the shortest processing time, update time, and record the index.
  5. Return the result.
class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        for i, t in enumerate(tasks):
            t.append(i)
        tasks.sort(key=lambda t: t[0])

        res, minHeap = [], []
        i, time = 0, tasks[0][0]

        while minHeap or i < len(tasks):
            while i < len(tasks) and time >= tasks[i][0]:
                heapq.heappush(minHeap, [tasks[i][1], tasks[i][2]])
                i += 1
            if not minHeap:
                time = tasks[i][0]
            else:
                procTime, index = heapq.heappop(minHeap)
                time += procTime
                res.append(index)
        return res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

3. Sorting + Min-Heap (Optimal)

Intuition

This solution optimizes memory by not modifying the original tasks array. Instead of copying task data, we sort an array of indices by enqueue time. The heap stores only indices and uses the original tasks array for comparisons. This approach is more memory efficient while maintaining the same time complexity.

Algorithm

  1. Create an array of indices [0, 1, 2, ..., n-1] and sort it based on the tasks' enqueue times.
  2. Initialize time to 0, and create a min-heap that compares indices by their task's processing time (then by index for ties).
  3. Iterate through sorted indices:
    • Push indices of tasks that have become available onto the heap.
    • If the heap is empty and tasks remain, jump time forward.
    • Otherwise, pop the best task, update time, and record the result.
  4. Return the execution order.
class Solution:
    def getOrder(self, tasks: List[List[int]]) -> List[int]:
        n = len(tasks)
        indices = list(range(n))
        indices.sort(key=lambda i: (tasks[i][0], i))

        class Task:
            def __init__(self, idx):
                self.idx = idx

            def __lt__(self, other):
                if tasks[self.idx][1] != tasks[other.idx][1]:
                    return tasks[self.idx][1] < tasks[other.idx][1]
                return self.idx < other.idx

        minHeap = []
        res = []
        time = i = 0
        while minHeap or i < n:
            while i < n and tasks[indices[i]][0] <= time:
                heapq.heappush(minHeap, Task(indices[i]))
                i += 1

            if not minHeap:
                time = tasks[indices[i]][0]
            else:
                next_task = heapq.heappop(minHeap)
                time += tasks[next_task.idx][1]
                res.append(next_task.idx)

        return res

Time & Space Complexity

  • Time complexity: O(nlogn)O(n \log n)
  • Space complexity: O(n)O(n)

Common Pitfalls

Losing Track of Original Indices

After sorting tasks by enqueue time, the original indices get shuffled. Failing to preserve and correctly reference the original task indices leads to returning the wrong execution order. Always attach the original index to each task before any sorting operation.

Incorrect Heap Comparison Priority

The heap must prioritize by processing time first, then by original index for ties. Reversing this order or forgetting the tiebreaker causes incorrect task selection when multiple tasks have the same processing time.

Integer Overflow in Time Calculation

When summing processing times, the cumulative time can exceed the 32-bit integer range for large inputs. Using a long or 64-bit integer for the time variable prevents overflow errors that lead to incorrect task availability checks.

Not Handling CPU Idle Time

When no tasks are available in the heap but tasks remain pending, the CPU must jump forward in time to the next task's enqueue time. Forgetting this case causes an infinite loop or incorrect scheduling when there are gaps between task arrivals.

Inefficient Available Task Selection

Using a list and sorting it each iteration instead of a proper min-heap results in O(n2logn)O(n^2 \log n) time complexity. The heap data structure is essential for efficiently selecting the minimum processing time task in O(logn)O(\log n) time.