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 timeenqueueTime_i
- Task
i
takesprocessingTime_i
units of time to complete
You have a single-threaded CPU that processes tasks according to these rules:
- When idle with no available tasks: The CPU stays idle and waits
- 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 - During processing: Once a task starts, it runs to completion without interruption
- 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]
.
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:
-
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 theirenqueueTime
. -
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 indicesq = []
: Min heap for available tasksi = 0
: Pointer to track which tasks we've consideredt = 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 EvaluatorExample 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 andn
pops =O(n × log n)
- Each
- 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
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
Recommended Readings
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!