Single-Threaded CPU
Description
You are given n tasks labeled from 0 to n - 1, represented by a 2D integer array tasks, where tasks[i] = [enqueueTime_i, processingTime_i].
This means the i-th task becomes available for processing at time enqueueTime_i and requires processingTime_i units of time to complete.
You have a single-threaded CPU that processes at most one task at a time. The CPU follows these rules:
- If the CPU is idle and no tasks are available, it stays idle.
- If the CPU is idle and tasks are available, it picks the task with the shortest processing time. If multiple tasks share the shortest processing time, it picks the one with the smallest original index.
- Once a task starts, the CPU runs it to completion without interruption.
- The CPU can finish one task and immediately start another.
Return the order in which the CPU processes the tasks.
Examples
Example 1
Input: tasks = [[1,2],[2,4],[3,2],[4,1]]
Output: [0,2,3,1]
Explanation:
- At time = 1, task 0 becomes available. The CPU picks task 0 (only option) and starts processing it.
- At time = 3, the CPU finishes task 0. By this time tasks 1 (enqueue=2) and 2 (enqueue=3) are available. Task 2 has processing time 2 which is shorter than task 1's processing time 4, so the CPU picks task 2.
- At time = 4, task 3 becomes available while the CPU is still running task 2.
- At time = 5, the CPU finishes task 2. Tasks 1 (processing=4) and 3 (processing=1) are available. Task 3 is shorter, so the CPU picks task 3.
- At time = 6, the CPU finishes task 3 and picks task 1 (the only remaining task).
- At time = 10, the CPU finishes task 1.
Processing order: [0, 2, 3, 1].
Example 2
Input: tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]]
Output: [4,3,2,0,1]
Explanation:
- At time = 7, all five tasks become available simultaneously. The CPU picks the task with the shortest processing time. Task 4 has processing time 2, which is the smallest.
- At time = 9, task 4 is done. Among remaining tasks, task 3 (processing=4) is shortest.
- At time = 13, task 3 is done. Task 2 (processing=5) is shortest.
- At time = 18, task 2 is done. Task 0 (processing=10) is shorter than task 1 (processing=12).
- At time = 28, task 0 is done. Task 1 is the only remaining task.
- At time = 40, task 1 is done.
Processing order: [4, 3, 2, 0, 1].
Constraints
tasks.length == n- 1 ≤ n ≤ 10^5
- 1 ≤ enqueueTime_i, processingTime_i ≤ 10^9
Editorial
Brute Force
Intuition
The most straightforward way to simulate this problem is to follow the CPU's behavior exactly as described. At each point in time when the CPU becomes idle, we scan through all tasks, find which ones are available (their enqueue time has passed), and among those pick the one with the shortest processing time (breaking ties by smallest index).
Think of it like a restaurant kitchen with one chef. Whenever the chef finishes a dish, they look at all the pending orders that have arrived so far, pick the quickest one to prepare, and start cooking. After finishing, they look at the queue again.
The key observation is: once the CPU finishes a task, we need to figure out which tasks have become available by the current time, then pick the best one. If no tasks are available, we jump time forward to the next task's enqueue time.
Step-by-Step Explanation
Let's trace through tasks = [[1,2],[2,4],[3,2],[4,1]]:
We keep track of current time, which tasks are done, and the result order.
Step 1: Initialize time = 0, no tasks processed yet. Scan all tasks — none have enqueueTime ≤ 0. Jump time to the smallest enqueueTime = 1.
Step 2: Time = 1. Scan all unprocessed tasks for enqueueTime ≤ 1. Only task 0 (enqueue=1, processing=2) qualifies. Pick task 0. Time advances to 1 + 2 = 3. Result: [0].
Step 3: Time = 3. Scan unprocessed tasks for enqueueTime ≤ 3. Task 1 (enqueue=2, processing=4) and task 2 (enqueue=3, processing=2) qualify. Task 2 has shorter processing time (2 < 4). Pick task 2. Time advances to 3 + 2 = 5. Result: [0, 2].
Step 4: Time = 5. Scan unprocessed tasks for enqueueTime ≤ 5. Task 1 (enqueue=2, processing=4) and task 3 (enqueue=4, processing=1) qualify. Task 3 has shorter processing time (1 < 4). Pick task 3. Time advances to 5 + 1 = 6. Result: [0, 2, 3].
Step 5: Time = 6. Scan unprocessed tasks for enqueueTime ≤ 6. Only task 1 remains (enqueue=2, processing=4). Pick task 1. Time advances to 6 + 4 = 10. Result: [0, 2, 3, 1].
Step 6: All tasks processed. Return [0, 2, 3, 1].
Brute Force — Scanning All Tasks at Each Step — Watch how the CPU scans all unprocessed tasks each time it becomes idle, picking the shortest-processing-time task among available ones.
Algorithm
- Pair each task with its original index so we can track ordering after sorting.
- Set current time = 0 and create a
processedboolean array. - Repeat until all tasks are processed:
a. Scan all unprocessed tasks to find those with enqueueTime ≤ current time.
b. Among available tasks, pick the one with the smallest processing time (break ties by smallest index).
c. If no tasks are available, jump time to the earliest unprocessed task's enqueue time.
d. Add the chosen task's index to the result. Advance time by its processing time. - Return the result array.
Code
class Solution {
public:
vector<int> getOrder(vector<vector<int>>& tasks) {
int n = tasks.size();
vector<int> result;
vector<bool> processed(n, false);
long long currentTime = 0;
for (int count = 0; count < n; count++) {
// Find available tasks
int bestIdx = -1;
int bestProc = INT_MAX;
long long minEnqueue = LLONG_MAX;
for (int i = 0; i < n; i++) {
if (processed[i]) continue;
minEnqueue = min(minEnqueue, (long long)tasks[i][0]);
if (tasks[i][0] <= currentTime) {
if (tasks[i][1] < bestProc ||
(tasks[i][1] == bestProc && i < bestIdx)) {
bestProc = tasks[i][1];
bestIdx = i;
}
}
}
// If no task available, jump time forward
if (bestIdx == -1) {
currentTime = minEnqueue;
count--; // Re-scan with updated time
continue;
}
processed[bestIdx] = true;
currentTime += bestProc;
result.push_back(bestIdx);
}
return result;
}
};class Solution:
def getOrder(self, tasks: list[list[int]]) -> list[int]:
n = len(tasks)
result = []
processed = [False] * n
current_time = 0
while len(result) < n:
best_idx = -1
best_proc = float('inf')
min_enqueue = float('inf')
for i in range(n):
if processed[i]:
continue
min_enqueue = min(min_enqueue, tasks[i][0])
if tasks[i][0] <= current_time:
if (tasks[i][1] < best_proc or
(tasks[i][1] == best_proc and i < best_idx)):
best_proc = tasks[i][1]
best_idx = i
if best_idx == -1:
current_time = min_enqueue
continue
processed[best_idx] = True
current_time += best_proc
result.append(best_idx)
return resultclass Solution {
public int[] getOrder(int[][] tasks) {
int n = tasks.length;
int[] result = new int[n];
boolean[] processed = new boolean[n];
long currentTime = 0;
int resultIdx = 0;
while (resultIdx < n) {
int bestIdx = -1;
int bestProc = Integer.MAX_VALUE;
long minEnqueue = Long.MAX_VALUE;
for (int i = 0; i < n; i++) {
if (processed[i]) continue;
minEnqueue = Math.min(minEnqueue, tasks[i][0]);
if (tasks[i][0] <= currentTime) {
if (tasks[i][1] < bestProc ||
(tasks[i][1] == bestProc && i < bestIdx)) {
bestProc = tasks[i][1];
bestIdx = i;
}
}
}
if (bestIdx == -1) {
currentTime = minEnqueue;
continue;
}
processed[bestIdx] = true;
currentTime += bestProc;
result[resultIdx++] = bestIdx;
}
return result;
}
}Complexity Analysis
Time Complexity: O(n²)
Each time the CPU becomes idle, we scan all n tasks to find the best available one. Since there are n tasks to process, and each idle event triggers a full scan, the total work is O(n) per task × n tasks = O(n²).
Space Complexity: O(n)
We use a processed boolean array of size n and a result array of size n. Both grow linearly with input size.
Why This Approach Is Not Efficient
The brute force approach scans all n tasks every time the CPU finishes a task. With n up to 10^5, this results in up to 10^10 operations in the worst case — far too slow for typical time limits.
The root inefficiency is that we repeatedly search through the entire task list to find the minimum processing time among available tasks. This is exactly the kind of operation a min-heap (priority queue) is designed for: it can give us the minimum element in O(log n) time instead of O(n).
Additionally, we scan tasks to find which ones have become available, which is wasteful. If we sort tasks by enqueue time first, we can efficiently add newly available tasks to the heap as time progresses, avoiding redundant scans.
By combining sorting (to process tasks in enqueue-time order) with a min-heap (to quickly extract the shortest-processing-time task), we can reduce the per-task work from O(n) to O(log n).
Optimal Approach - Sorting + Min-Heap
Intuition
Instead of scanning all tasks repeatedly, we can be much smarter:
-
Sort tasks by enqueue time so we can efficiently determine which tasks have become available as time progresses. We just need a pointer that advances through the sorted array.
-
Use a min-heap to maintain the pool of available tasks. The heap is ordered by (processingTime, originalIndex) so the best task to run is always at the top.
The simulation proceeds as follows: whenever the CPU is idle, we add all tasks whose enqueue time has passed into the heap. Then we pop the top of the heap (shortest processing time, smallest index as tiebreaker) and run that task. If the heap is empty and tasks remain, we fast-forward time to the next task's enqueue time.
Think of it like an airport gate with a priority boarding system. Passengers (tasks) arrive at different times. As each passenger arrives, they join a priority queue sorted by their ticket class (processing time). When the gate agent (CPU) is ready, they call the highest-priority passenger (shortest processing time). If nobody is in line yet, the agent waits until the next passenger arrives.
Step-by-Step Explanation
Let's trace through tasks = [[1,2],[2,4],[3,2],[4,1]]:
First, pair each task with its original index and sort by enqueue time:
sorted_tasks = [(1,2,0), (2,4,1), (3,2,2), (4,1,3)] — already sorted here.
Step 1: Initialize time = 0, heap = [], pointer = 0. No tasks have enqueue ≤ 0. Fast-forward time to sorted_tasks[0].enqueue = 1.
Step 2: Time = 1. Add all tasks with enqueue ≤ 1: task 0 (proc=2, idx=0). Heap = [(2, 0)]. Pointer moves to 1.
Step 3: Pop from heap: (2, 0) → task 0. CPU runs task 0 for 2 units. Time = 1 + 2 = 3. Result: [0].
Step 4: Time = 3. Add tasks with enqueue ≤ 3: task 1 (proc=4, idx=1) and task 2 (proc=2, idx=2). Heap = [(2, 2), (4, 1)]. Pointer moves to 3.
Step 5: Pop from heap: (2, 2) → task 2. CPU runs task 2 for 2 units. Time = 3 + 2 = 5. Result: [0, 2].
Step 6: Time = 5. Add tasks with enqueue ≤ 5: task 3 (proc=1, idx=3). Heap = [(1, 3), (4, 1)]. Pointer moves to 4.
Step 7: Pop from heap: (1, 3) → task 3. CPU runs task 3 for 1 unit. Time = 5 + 1 = 6. Result: [0, 2, 3].
Step 8: Time = 6. No new tasks (pointer = 4, all tasks added). Heap = [(4, 1)].
Step 9: Pop from heap: (4, 1) → task 1. CPU runs task 1 for 4 units. Time = 6 + 4 = 10. Result: [0, 2, 3, 1].
Step 10: Heap is empty, all tasks processed. Return [0, 2, 3, 1].
Sorting + Min-Heap — Efficient Task Scheduling — Watch how sorting tasks by enqueue time and using a min-heap (keyed on processing time, then index) lets us efficiently pick the best task at each step without scanning all tasks.
Algorithm
- Attach the original index to each task: create tuples (enqueueTime, processingTime, originalIndex).
- Sort these tuples by enqueueTime.
- Initialize: time = 0, pointer = 0, result = [], min-heap = [].
- While result has fewer than n elements:
a. Add all tasks with enqueueTime ≤ current time to the heap (keyed by processingTime, then index).
b. If the heap is empty and pointer < n, fast-forward time to sorted_tasks[pointer].enqueueTime and go to step 4a.
c. Pop the top of the heap (smallest processing time, smallest index).
d. Advance time by the popped task's processing time.
e. Append the popped task's original index to result. - Return result.
Code
class Solution {
public:
vector<int> getOrder(vector<vector<int>>& tasks) {
int n = tasks.size();
// Pair each task with its original index
vector<array<long long, 3>> sortedTasks(n);
for (int i = 0; i < n; i++) {
sortedTasks[i] = {tasks[i][0], tasks[i][1], i};
}
// Sort by enqueue time
sort(sortedTasks.begin(), sortedTasks.end());
// Min-heap: (processingTime, originalIndex)
priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> minHeap;
vector<int> result;
long long currentTime = 0;
int ptr = 0;
while (result.size() < n) {
// Add all tasks that have arrived by currentTime
while (ptr < n && sortedTasks[ptr][0] <= currentTime) {
minHeap.push({sortedTasks[ptr][1], (int)sortedTasks[ptr][2]});
ptr++;
}
if (minHeap.empty()) {
// No available tasks — jump to next enqueue time
currentTime = sortedTasks[ptr][0];
continue;
}
auto [procTime, idx] = minHeap.top();
minHeap.pop();
currentTime += procTime;
result.push_back(idx);
}
return result;
}
};import heapq
class Solution:
def getOrder(self, tasks: list[list[int]]) -> list[int]:
n = len(tasks)
# Pair each task with its original index and sort by enqueue time
sorted_tasks = sorted(
[(enqueue, proc, idx) for idx, (enqueue, proc) in enumerate(tasks)]
)
result = []
min_heap = [] # (processingTime, originalIndex)
current_time = 0
ptr = 0
while len(result) < n:
# Add all tasks that have arrived by current_time
while ptr < n and sorted_tasks[ptr][0] <= current_time:
heapq.heappush(min_heap, (sorted_tasks[ptr][1], sorted_tasks[ptr][2]))
ptr += 1
if not min_heap:
# No available tasks — jump to next enqueue time
current_time = sorted_tasks[ptr][0]
continue
proc_time, idx = heapq.heappop(min_heap)
current_time += proc_time
result.append(idx)
return resultclass Solution {
public int[] getOrder(int[][] tasks) {
int n = tasks.length;
// Pair each task with its original index
int[][] sortedTasks = new int[n][3];
for (int i = 0; i < n; i++) {
sortedTasks[i][0] = tasks[i][0]; // enqueueTime
sortedTasks[i][1] = tasks[i][1]; // processingTime
sortedTasks[i][2] = i; // originalIndex
}
// Sort by enqueue time
Arrays.sort(sortedTasks, (a, b) -> Integer.compare(a[0], b[0]));
// Min-heap: (processingTime, originalIndex)
PriorityQueue<int[]> minHeap = new PriorityQueue<>((a, b) -> {
if (a[0] != b[0]) return Integer.compare(a[0], b[0]);
return Integer.compare(a[1], b[1]);
});
int[] result = new int[n];
long currentTime = 0;
int ptr = 0, resultIdx = 0;
while (resultIdx < n) {
// Add all tasks that have arrived by currentTime
while (ptr < n && sortedTasks[ptr][0] <= currentTime) {
minHeap.offer(new int[]{sortedTasks[ptr][1], sortedTasks[ptr][2]});
ptr++;
}
if (minHeap.isEmpty()) {
// No available tasks — jump to next enqueue time
currentTime = sortedTasks[ptr][0];
continue;
}
int[] task = minHeap.poll();
currentTime += task[0];
result[resultIdx++] = task[1];
}
return result;
}
}Complexity Analysis
Time Complexity: O(n log n)
Sorting the tasks takes O(n log n). Each task is inserted into and extracted from the heap exactly once. Each heap operation (push or pop) takes O(log n). Since there are n tasks, the total heap work is O(n log n). The pointer ptr advances from 0 to n over the entire execution — never going backward — so the inner while loop runs O(n) total across all iterations. Overall: O(n log n) + O(n log n) = O(n log n).
Space Complexity: O(n)
The sorted task array uses O(n) space. The heap can hold up to n elements in the worst case (when all tasks arrive simultaneously). The result array is O(n). Total auxiliary space: O(n).