Facebook Pixel

1834. Single-Threaded CPU

Problem Description

You have n tasks numbered from 0 to n - 1. Each task is described by a 2D array tasks, where tasks[i] = [enqueueTime_i, processingTime_i]. This means:

  • Task i becomes available at time enqueueTime_i
  • Task i takes processingTime_i units of time to complete

You have a single-threaded CPU that processes tasks according to these rules:

  1. When idle with no available tasks: The CPU stays idle and waits
  2. When idle with available tasks: The CPU picks the task with the shortest processingTime. If there's a tie, it picks the task with the smallest index
  3. During processing: Once a task starts, it runs to completion without interruption
  4. After completion: The CPU can immediately start the next task

Your goal is to determine and return the order in which the CPU will process all tasks.

For example, if you have tasks [[1,2], [2,4], [3,2], [4,1]]:

  • At time 1, task 0 becomes available and starts (no other choices)
  • At time 3 (after task 0 finishes), tasks 1 and 2 are available. Task 2 has shorter processing time (2 vs 4), so it runs next
  • At time 5, tasks 1 and 3 are available. Task 3 has shorter processing time (1 vs 4), so it runs next
  • Finally, task 1 runs

The processing order would be [0, 2, 3, 1].

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that this problem simulates a CPU scheduler that always picks the shortest job available. At any given moment, we need to know which tasks are available and quickly select the one with minimum processing time.

Think about what happens in real time: tasks become available at different times, and we can only consider tasks that have already "arrived" (their enqueueTime has passed). Among these available tasks, we want to efficiently find and extract the one with minimum processing time.

This naturally leads us to two important observations:

  1. We need to process tasks in time order: We should look at tasks as they become available, which suggests sorting tasks by their enqueueTime. This way, we can iterate through tasks chronologically and add them to our available pool when the current time reaches their enqueueTime.

  2. We need quick access to the minimum: Among available tasks, we repeatedly need to find and remove the one with shortest processing time. This is exactly what a min heap (priority queue) does efficiently - it maintains elements so we can always extract the minimum in O(log n) time.

The simulation flow becomes clear:

  • Sort tasks by arrival time so we can process them chronologically
  • Maintain current time t as we execute tasks
  • Use a min heap to store available tasks (ordered by processing time)
  • When the CPU is free, add all newly available tasks to the heap, then pick the shortest one
  • Update the current time after each task completes
  • If no tasks are available, jump forward in time to when the next task arrives

We need to track the original indices since the problem asks for the order of task indices, not the tasks themselves. This is why we append the original index to each task before sorting.

Learn more about Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation follows a simulation approach using sorting and a min heap:

1. Preprocessing - Add Original Indices:

for i, task in enumerate(tasks):
    task.append(i)

We append each task's original index to preserve it after sorting, transforming each task from [enqueueTime, processingTime] to [enqueueTime, processingTime, originalIndex].

2. Sort Tasks by Enqueue Time:

tasks.sort()

This ensures we can process tasks chronologically as they become available.

3. Initialize Variables:

  • ans = []: Stores the order of task indices
  • q = []: Min heap for available tasks
  • i = 0: Pointer to track which tasks we've considered
  • t = 0: Current time

4. Main Simulation Loop:

while q or i < n:

Continue while there are tasks in the heap or unprocessed tasks remaining.

5. Handle Idle Time:

if not q:
    t = max(t, tasks[i][0])

If the heap is empty (no available tasks), jump time forward to either the current time or the next task's enqueueTime, whichever is later.

6. Add Available Tasks to Heap:

while i < n and tasks[i][0] <= t:
    heappush(q, (tasks[i][1], tasks[i][2]))
    i += 1

Add all tasks whose enqueueTime is less than or equal to current time t to the min heap. The heap stores tuples (processingTime, originalIndex) so it naturally orders by processing time first, then by index if there's a tie.

7. Process Next Task:

pt, j = heappop(q)
ans.append(j)
t += pt

Extract the task with minimum processing time from the heap, add its index to the result, and advance time by the task's processing duration.

Time Complexity: O(n log n) - dominated by the initial sort and heap operations (each task is pushed and popped once).

Space Complexity: O(n) - for the heap and result array.

The beauty of this solution is how it naturally handles all edge cases: gaps in task availability, multiple tasks becoming available simultaneously, and the tie-breaking rule for tasks with equal processing times.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with tasks = [[1,2], [2,4], [3,2], [4,1]].

Step 1: Add original indices

  • Task 0: [1,2][1,2,0]
  • Task 1: [2,4][2,4,1]
  • Task 2: [3,2][3,2,2]
  • Task 3: [4,1][4,1,3]

Step 2: Sort by enqueue time After sorting: [[1,2,0], [2,4,1], [3,2,2], [4,1,3]] (already sorted in this case)

Step 3: Initialize

  • ans = [], q = [] (min heap), i = 0, t = 0

Step 4: Main simulation

Iteration 1:

  • Heap is empty, so t = max(0, tasks[0][0]) = 1
  • Add tasks with enqueueTime ≤ 1: task [1,2,0]
  • Heap: [(2,0)]
  • Pop (2,0): process task 0
  • ans = [0], t = 1 + 2 = 3

Iteration 2:

  • Add tasks with enqueueTime ≤ 3: tasks [2,4,1] and [3,2,2]
  • Heap: [(2,2), (4,1)] (ordered by processing time)
  • Pop (2,2): process task 2
  • ans = [0,2], t = 3 + 2 = 5

Iteration 3:

  • Add tasks with enqueueTime ≤ 5: task [4,1,3]
  • Heap: [(1,3), (4,1)]
  • Pop (1,3): process task 3
  • ans = [0,2,3], t = 5 + 1 = 6

Iteration 4:

  • No new tasks to add (all processed)
  • Heap: [(4,1)]
  • Pop (4,1): process task 1
  • ans = [0,2,3,1], t = 6 + 4 = 10

Final Result: [0, 2, 3, 1]

The CPU processes task 0 first (only option at time 1), then task 2 (shorter than task 1), then task 3 (shortest available), and finally task 1.

Solution Implementation

1from typing import List
2import heapq
3
4class Solution:
5    def getOrder(self, tasks: List[List[int]]) -> List[int]:
6        # Add original index to each task for tracking
7        # Each task becomes [enqueue_time, processing_time, original_index]
8        for index, task in enumerate(tasks):
9            task.append(index)
10      
11        # Sort tasks by enqueue time (first available time)
12        tasks.sort()
13      
14        # Initialize result list to store order of task execution
15        result = []
16      
17        # Priority queue to store available tasks (min-heap by processing time, then index)
18        available_tasks = []
19      
20        # Total number of tasks
21        num_tasks = len(tasks)
22      
23        # Current task index and current time
24        task_index = 0
25        current_time = 0
26      
27        # Process all tasks until both queue is empty and all tasks are processed
28        while available_tasks or task_index < num_tasks:
29            # If no tasks are available, jump to the next task's enqueue time
30            if not available_tasks:
31                current_time = max(current_time, tasks[task_index][0])
32          
33            # Add all tasks that have become available at current time to the heap
34            while task_index < num_tasks and tasks[task_index][0] <= current_time:
35                # Push (processing_time, original_index) to maintain priority
36                heapq.heappush(available_tasks, (tasks[task_index][1], tasks[task_index][2]))
37                task_index += 1
38          
39            # Process the task with shortest processing time (and smallest index if tied)
40            processing_time, original_index = heapq.heappop(available_tasks)
41            result.append(original_index)
42          
43            # Update current time after processing the task
44            current_time += processing_time
45      
46        return result
47
1class Solution {
2    public int[] getOrder(int[][] tasks) {
3        int n = tasks.length;
4      
5        // Create extended tasks array: [enqueueTime, processingTime, originalIndex]
6        int[][] extendedTasks = new int[n][3];
7        for (int i = 0; i < n; i++) {
8            extendedTasks[i] = new int[] {tasks[i][0], tasks[i][1], i};
9        }
10      
11        // Sort tasks by enqueue time (earliest first)
12        Arrays.sort(extendedTasks, (a, b) -> a[0] - b[0]);
13      
14        // Result array to store the order of task execution
15        int[] result = new int[n];
16      
17        // Min heap: prioritize by processing time, then by index if tied
18        // Stores [processingTime, originalIndex]
19        PriorityQueue<int[]> availableTasksQueue = new PriorityQueue<>((a, b) -> {
20            if (a[0] == b[0]) {
21                return a[1] - b[1];  // If processing times equal, sort by index
22            }
23            return a[0] - b[0];  // Sort by processing time
24        });
25      
26        int taskIndex = 0;        // Index for iterating through sorted tasks
27        int currentTime = 0;      // Current CPU time
28        int resultIndex = 0;      // Index for filling result array
29      
30        // Process all tasks
31        while (!availableTasksQueue.isEmpty() || taskIndex < n) {
32            // If no tasks available, fast-forward time to next task's enqueue time
33            if (availableTasksQueue.isEmpty()) {
34                currentTime = Math.max(currentTime, extendedTasks[taskIndex][0]);
35            }
36          
37            // Add all tasks that have become available by current time
38            while (taskIndex < n && extendedTasks[taskIndex][0] <= currentTime) {
39                availableTasksQueue.offer(new int[] {
40                    extendedTasks[taskIndex][1],  // Processing time
41                    extendedTasks[taskIndex][2]   // Original index
42                });
43                taskIndex++;
44            }
45          
46            // Process the next optimal task (shortest processing time, lowest index)
47            int[] nextTask = availableTasksQueue.poll();
48            result[resultIndex++] = nextTask[1];  // Store original index
49            currentTime += nextTask[0];           // Update time after processing
50        }
51      
52        return result;
53    }
54}
55
1class Solution {
2public:
3    vector<int> getOrder(vector<vector<int>>& tasks) {
4        int numTasks = tasks.size();
5      
6        // Add original index to each task for tracking
7        // tasks[i] = {enqueueTime, processingTime, originalIndex}
8        for (int i = 0; i < numTasks; i++) {
9            tasks[i].push_back(i);
10        }
11      
12        // Sort tasks by enqueue time (arrival time)
13        sort(tasks.begin(), tasks.end());
14      
15        // Min heap to store available tasks
16        // Priority: first by processing time, then by original index
17        using TaskPair = pair<int, int>;  // {processingTime, originalIndex}
18        priority_queue<TaskPair, vector<TaskPair>, greater<TaskPair>> availableTasks;
19      
20        int taskIndex = 0;
21        long long currentTime = 0;
22        vector<int> result;
23      
24        // Process all tasks
25        while (!availableTasks.empty() || taskIndex < numTasks) {
26            // If no tasks available, jump to next task's enqueue time
27            if (availableTasks.empty()) {
28                currentTime = max(currentTime, (long long)tasks[taskIndex][0]);
29            }
30          
31            // Add all tasks that have arrived by current time to the heap
32            while (taskIndex < numTasks && tasks[taskIndex][0] <= currentTime) {
33                int processingTime = tasks[taskIndex][1];
34                int originalIndex = tasks[taskIndex][2];
35                availableTasks.push({processingTime, originalIndex});
36                taskIndex++;
37            }
38          
39            // Process the task with minimum processing time (and minimum index if tied)
40            auto [processingTime, originalIndex] = availableTasks.top();
41            availableTasks.pop();
42            result.push_back(originalIndex);
43          
44            // Update current time after processing the task
45            currentTime += processingTime;
46        }
47      
48        return result;
49    }
50};
51
1function getOrder(tasks: number[][]): number[] {
2    const numTasks = tasks.length;
3  
4    // Add original index to each task for tracking
5    // tasks[i] = [enqueueTime, processingTime, originalIndex]
6    for (let i = 0; i < numTasks; i++) {
7        tasks[i].push(i);
8    }
9  
10    // Sort tasks by enqueue time (arrival time)
11    tasks.sort((a, b) => a[0] - b[0]);
12  
13    // Min heap to store available tasks
14    // Priority: first by processing time, then by original index
15    // Each element: [processingTime, originalIndex]
16    const availableTasks: number[][] = [];
17  
18    // Helper function to maintain min heap property when adding element
19    const heapPush = (heap: number[][], item: number[]): void => {
20        heap.push(item);
21        let currentIndex = heap.length - 1;
22      
23        // Bubble up to maintain heap property
24        while (currentIndex > 0) {
25            const parentIndex = Math.floor((currentIndex - 1) / 2);
26            // Compare first by processing time, then by original index
27            if (heap[currentIndex][0] < heap[parentIndex][0] || 
28                (heap[currentIndex][0] === heap[parentIndex][0] && 
29                 heap[currentIndex][1] < heap[parentIndex][1])) {
30                [heap[currentIndex], heap[parentIndex]] = [heap[parentIndex], heap[currentIndex]];
31                currentIndex = parentIndex;
32            } else {
33                break;
34            }
35        }
36    };
37  
38    // Helper function to maintain min heap property when removing root
39    const heapPop = (heap: number[][]): number[] => {
40        if (heap.length === 0) return [];
41      
42        const result = heap[0];
43        heap[0] = heap[heap.length - 1];
44        heap.pop();
45      
46        if (heap.length === 0) return result;
47      
48        // Bubble down to maintain heap property
49        let currentIndex = 0;
50        while (true) {
51            let minIndex = currentIndex;
52            const leftChild = 2 * currentIndex + 1;
53            const rightChild = 2 * currentIndex + 2;
54          
55            // Check left child
56            if (leftChild < heap.length) {
57                if (heap[leftChild][0] < heap[minIndex][0] || 
58                    (heap[leftChild][0] === heap[minIndex][0] && 
59                     heap[leftChild][1] < heap[minIndex][1])) {
60                    minIndex = leftChild;
61                }
62            }
63          
64            // Check right child
65            if (rightChild < heap.length) {
66                if (heap[rightChild][0] < heap[minIndex][0] || 
67                    (heap[rightChild][0] === heap[minIndex][0] && 
68                     heap[rightChild][1] < heap[minIndex][1])) {
69                    minIndex = rightChild;
70                }
71            }
72          
73            if (minIndex !== currentIndex) {
74                [heap[currentIndex], heap[minIndex]] = [heap[minIndex], heap[currentIndex]];
75                currentIndex = minIndex;
76            } else {
77                break;
78            }
79        }
80      
81        return result;
82    };
83  
84    let taskIndex = 0;
85    let currentTime = 0;
86    const result: number[] = [];
87  
88    // Process all tasks
89    while (availableTasks.length > 0 || taskIndex < numTasks) {
90        // If no tasks available, jump to next task's enqueue time
91        if (availableTasks.length === 0) {
92            currentTime = Math.max(currentTime, tasks[taskIndex][0]);
93        }
94      
95        // Add all tasks that have arrived by current time to the heap
96        while (taskIndex < numTasks && tasks[taskIndex][0] <= currentTime) {
97            const processingTime = tasks[taskIndex][1];
98            const originalIndex = tasks[taskIndex][2];
99            heapPush(availableTasks, [processingTime, originalIndex]);
100            taskIndex++;
101        }
102      
103        // Process the task with minimum processing time (and minimum index if tied)
104        const [processingTime, originalIndex] = heapPop(availableTasks);
105        result.push(originalIndex);
106      
107        // Update current time after processing the task
108        currentTime += processingTime;
109    }
110  
111    return result;
112}
113

Time and Space Complexity

Time Complexity: O(n × log n), where n is the number of tasks.

The time complexity breaks down as follows:

  • Adding indices to tasks: O(n) - iterating through all tasks once
  • Sorting tasks by enqueue time: O(n × log n) - standard comparison-based sorting
  • Main while loop processes each task exactly once: O(n) iterations total
    • Each heappush operation: O(log n) in worst case
    • Each heappop operation: O(log n) in worst case
    • Total heap operations: n pushes and n pops = O(n × log n)
  • Overall: O(n) + O(n × log n) + O(n × log n) = O(n × log n)

Space Complexity: O(n)

The space complexity consists of:

  • Modified tasks list with added indices: O(n) - stores all tasks with their original indices
  • Priority queue q: O(n) in worst case - can contain all tasks if they all have the same enqueue time
  • Answer list ans: O(n) - stores indices of all tasks
  • Other variables (i, t, pt, j): O(1)
  • Overall: O(n) + O(n) + O(n) + O(1) = O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Integer Overflow with Large Time Values

One critical pitfall is that time values can accumulate to very large numbers. If enqueueTime and processingTime values are near their maximum (up to 10^9 according to typical constraints), the cumulative time can exceed standard integer limits in some languages.

Problem Example:

tasks = [[1000000000, 1000000000], [1, 1000000000]]
# After processing task 0, current_time becomes 2000000000
# This could cause overflow in languages with fixed integer sizes

Solution: In Python, integers have arbitrary precision so overflow isn't an issue. However, in other languages like Java or C++, use long instead of int:

long currentTime = 0;  // Use long instead of int

2. Modifying Input Array In-Place

The code modifies the input tasks array by appending indices to it. This can cause issues if:

  • The input needs to be preserved for other operations
  • The function is called multiple times with the same input
  • Testing/debugging becomes harder with mutated inputs

Problem Example:

solution = Solution()
tasks = [[1,2], [2,4]]
result1 = solution.getOrder(tasks)  # tasks is now [[1,2,0], [2,4,1]]
result2 = solution.getOrder(tasks)  # Error! tasks already has indices appended

Solution: Create a new list instead of modifying the input:

def getOrder(self, tasks: List[List[int]]) -> List[int]:
    # Create a new list with indices instead of modifying input
    indexed_tasks = [[task[0], task[1], i] for i, task in enumerate(tasks)]
    indexed_tasks.sort()
    # ... rest of the code uses indexed_tasks instead of tasks

3. Incorrect Heap Priority for Tie-Breaking

When using a heap with tuples, Python compares elements lexicographically. If only pushing (processingTime, index) without careful consideration, the tie-breaking might work correctly by accident. However, relying on implicit behavior can lead to bugs if the data structure changes.

Problem Example: If someone mistakenly pushes (processingTime, enqueueTime, index) thinking more information is better:

# Wrong! This would break tie by enqueueTime instead of index
heapq.heappush(available_tasks, (tasks[i][1], tasks[i][0], tasks[i][2]))

Solution: Be explicit about what goes into the heap and document why:

# Heap stores exactly (processing_time, original_index)
# This ensures: 1) Primary sort by processing_time
#              2) Secondary sort by index (for ties)
heapq.heappush(available_tasks, (tasks[task_index][1], tasks[task_index][2]))

4. Not Handling Empty Task List

While the problem typically guarantees n >= 1, defensive programming suggests handling edge cases.

Problem Example:

tasks = []
# The code would work but it's good practice to handle explicitly

Solution: Add an early return for empty input:

def getOrder(self, tasks: List[List[int]]) -> List[int]:
    if not tasks:
        return []
    # ... rest of the code
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More