Single-Threaded CPU
MedPrompt
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 enqueueTimei and will take processingTimei 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, 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,2],[2,4],[3,2],[4,1]]
Output: [0,2,3,1]
Explanation: The events go as follows:
- At time = 1, task 0 is available to process. Available tasks = {0}.
- Also at time = 1, the idle CPU starts processing task 0. Available tasks = {}.
- At time = 2, task 1 is available to process. Available tasks = {1}.
- At time = 3, task 2 is available to process. Available tasks = {1, 2}.
- Also at time = 3, the CPU finishes task 0 and starts processing task 2 as it is the shortest. Available tasks = {1}.
- At time = 4, task 3 is available to process. Available tasks = {1, 3}.
- At time = 5, the CPU finishes task 2 and starts processing task 3 as it is the shortest. Available tasks = {1}.
- At time = 6, the CPU finishes task 3 and starts processing task 1. Available tasks = {}.
- At time = 10, the CPU finishes task 1 and becomes idle.Example 2:
Input: tasks = [[7,10],[7,12],[7,5],[7,4],[7,2]]
Output: [4,3,2,0,1]
Explanation: The events go as follows:
- At time = 7, all the tasks become available. Available tasks = {0,1,2,3,4}.
- Also at time = 7, the idle CPU starts processing task 4. Available tasks = {0,1,2,3}.
- At time = 9, the CPU finishes task 4 and starts processing task 3. Available tasks = {0,1,2}.
- At time = 13, the CPU finishes task 3 and starts processing task 2. Available tasks = {0,1}.
- At time = 18, the CPU finishes task 2 and starts processing task 0. Available tasks = {1}.
- At time = 28, the CPU finishes task 0 and starts processing task 1. Available tasks = {}.
- At time = 40, the CPU finishes task 1 and becomes idle.
Constraints:
tasks.length == n1 <= n <= 1051 <= enqueueTimei, processingTimei <= 109
Approaches
2 approaches with complexity analysis and trade-offs.
This approach directly simulates the process described in the problem statement without using any advanced data structures for optimization. It iterates through time, and at each step where the CPU is free, it scans all tasks to determine which ones are available and then scans the available tasks again to find the one with the shortest processing time.
Algorithm
- Initialize
currentTime = 0, a boolean arrayprocessedof sizento track completed tasks, and an integer arrayresultto store the output. - Loop
ntimes, as we need to selectntasks in total. - In each iteration, find the set of tasks that are available to be processed. A task
iis available if it's not yet processed (processed[i]is false) and itsenqueueTimeis less than or equal tocurrentTime. - Case 1: No tasks are available. If the set of available tasks is empty, the CPU is idle. We must advance time. Find the minimum
enqueueTimeamong all unprocessed tasks and setcurrentTimeto this value. Then, repeat step 3 to find available tasks at this new time. - Case 2: Tasks are available. From the set of available tasks, iterate through them to find the best one to execute. The best task is the one with the minimum
processingTime. If there's a tie inprocessingTime, choose the one with the smaller original index. - Once the best task (say, index
j) is identified, addjto theresultarray. - Mark task
jas processed by settingprocessed[j] = true. - Update
currentTimeby adding theprocessingTimeof taskjto it. - Repeat the loop until
ntasks have been added to theresultarray.
Walkthrough
The brute-force method involves a main loop that runs n times, once for each task to be scheduled. Inside this loop, we perform a linear scan over the entire tasks array to find all tasks that are currently available (i.e., their enqueue time is not later than the current time). If no tasks are available, we advance the current time to the earliest arrival time of any remaining task. From the pool of available tasks, we perform another linear scan to select the one with the minimum processing time, breaking ties with the task's original index. This selected task is added to our result, and the current time is updated. This process repeats until all tasks are scheduled.
class Solution { public int[] getOrder(int[][] tasks) { int n = tasks.length; int[] result = new int[n]; boolean[] processed = new boolean[n]; long currentTime = 1; int resultIndex = 0; while (resultIndex < n) { int bestTaskIndex = -1; long minProcessingTime = Long.MAX_VALUE; // Find the best available task at the current time for (int i = 0; i < n; i++) { if (!processed[i] && tasks[i][0] <= currentTime) { if (tasks[i][1] < minProcessingTime) { minProcessingTime = tasks[i][1]; bestTaskIndex = i; } else if (tasks[i][1] == minProcessingTime) { if (i < bestTaskIndex) { bestTaskIndex = i; } } } } // If no task is available, CPU is idle. Fast-forward time. if (bestTaskIndex == -1) { long nextEnqueueTime = Long.MAX_VALUE; for (int i = 0; i < n; i++) { if (!processed[i]) { nextEnqueueTime = Math.min(nextEnqueueTime, tasks[i][0]); } } currentTime = nextEnqueueTime; } else { // Process the best task found result[resultIndex++] = bestTaskIndex; processed[bestTaskIndex] = true; currentTime += tasks[bestTaskIndex][1]; } } return result; }}Complexity
Time
O(n^2), where n is the number of tasks. The main loop runs `n` times. Inside, finding available tasks and then the best task among them can take up to O(n) time in each iteration, leading to a quadratic time complexity.
Space
O(n) to store the `processed` status of each task and the `result` array.
Trade-offs
Pros
Simple to understand as it directly models the problem's rules.
Requires minimal complex data structures.
Cons
Highly inefficient due to nested loops.
Will result in a 'Time Limit Exceeded' (TLE) error on platforms like LeetCode for the given constraints.
This optimal approach improves efficiency by avoiding repeated scans of the task list. We first sort the tasks by their enqueueTime. Then, we simulate the process using a Min-Heap (Priority Queue) to maintain the pool of available tasks. The heap allows us to efficiently retrieve the task with the shortest processing time in O(log n) time, which is a significant improvement over the O(n) scan in the brute-force method.
Algorithm
- Augment and Sort: Since the original index is needed for tie-breaking and the final output, we first transform the input
tasksarray. Create a new array of objects or arrays, where each element stores[enqueueTime, processingTime, originalIndex]. Sort this new array based onenqueueTimein ascending order. - Initialize Data Structures:
- A
Min-Heap(Priority Queue) to store available tasks. The heap will order tasks first byprocessingTime(ascending) and then byoriginalIndex(ascending) for tie-breaking. - A
resultarray to store the final order of task indices. - A variable
currentTime(as alongto prevent overflow), initialized to 0. - A pointer
taskIndexto traverse the sorted tasks array, initialized to 0.
- A
- Processing Loop: Loop until the
resultarray is full (i.e., allntasks have been scheduled).- Add Available Tasks: Add all tasks from the sorted array whose
enqueueTimeis less than or equal tocurrentTimeinto the min-heap. IncrementtaskIndexas you add them. - Check CPU State:
- If the min-heap is empty, it means the CPU is idle. Fast-forward
currentTimeto theenqueueTimeof the next task in the sorted list (sortedTasks[taskIndex][0]). - If the min-heap is not empty, the CPU is busy. Poll the task with the highest priority (shortest
processingTime/smallestindex) from the heap.
- If the min-heap is empty, it means the CPU is idle. Fast-forward
- Update State: Add the polled task's
originalIndexto theresultarray. AdvancecurrentTimeby the task'sprocessingTime.
- Add Available Tasks: Add all tasks from the sorted array whose
- Return Result: Once the loop completes, the
resultarray contains the correct processing order.
Walkthrough
The key idea is to process events in chronological order. Sorting tasks by enqueueTime ensures we consider tasks as they become available. A min-heap is the perfect data structure for managing the set of available tasks, as it always provides the next task to be processed (shortest processingTime, then smallest index) in logarithmic time.
We maintain a currentTime variable. The main loop continues until all tasks are scheduled. In each phase, we first add all tasks that have become available (enqueueTime <= currentTime) from our sorted list into the min-heap. If the heap is empty, the CPU is idle, so we jump currentTime to the arrival of the next scheduled task. Otherwise, we extract the best task from the heap, add it to our result, and advance currentTime by its processing duration. This ensures that at every decision point, we make the optimal choice according to the problem's rules efficiently.
import java.util.Arrays;import java.util.PriorityQueue; class Solution { public int[] getOrder(int[][] tasks) { int n = tasks.length; // Create an array of tasks with their original indices: [enqueueTime, processingTime, originalIndex] int[][] indexedTasks = new int[n][3]; for (int i = 0; i < n; i++) { indexedTasks[i][0] = tasks[i][0]; indexedTasks[i][1] = tasks[i][1]; indexedTasks[i][2] = i; } // Sort tasks by enqueueTime Arrays.sort(indexedTasks, (a, b) -> Integer.compare(a[0], b[0])); // Min-heap to store available tasks: [processingTime, originalIndex] // Prioritize by processingTime, then by index PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> { if (a[0] != b[0]) { return Integer.compare(a[0], b[0]); // Shorter processing time first } else { return Integer.compare(a[1], b[1]); // Smaller index first for ties } }); int[] result = new int[n]; int resultIndex = 0; int taskIndex = 0; long currentTime = 0; while (resultIndex < n) { // Add all tasks that have become available by currentTime to the heap while (taskIndex < n && indexedTasks[taskIndex][0] <= currentTime) { pq.offer(new int[]{indexedTasks[taskIndex][1], indexedTasks[taskIndex][2]}); taskIndex++; } // If the CPU is idle and no tasks are available, fast-forward time if (pq.isEmpty()) { currentTime = indexedTasks[taskIndex][0]; } else { // Process the next task from the heap int[] nextTask = pq.poll(); int processingTime = nextTask[0]; int originalIndex = nextTask[1]; result[resultIndex++] = originalIndex; currentTime += processingTime; } } return result; }}Complexity
Time
O(n log n). The initial sorting of the tasks takes O(n log n). The main loop processes each task once. Each task is pushed onto the heap (O(log n)) and popped from the heap (O(log n)) exactly once. Thus, the total time complexity is dominated by the sorting and heap operations.
Space
O(n). We need O(n) space for the `indexedTasks` array. The min-heap can, in the worst-case scenario, hold all `n` tasks (if they all become available at the same time). The `result` array also takes O(n) space.
Trade-offs
Pros
Highly efficient, with a time complexity of O(n log n).
Correctly and optimally solves the problem within the given constraints.
It's a standard and robust pattern for solving discrete-event simulation and scheduling problems.
Cons
Requires more memory due to the sorted copy of tasks and the heap.
Implementation is more complex than the brute-force approach.
Solutions
Solution
class Solution { public int [] getOrder ( int [][] tasks ) { int n = tasks . length ; int [][] ts = new int [ n ][ 3 ]; for ( int i = 0 ; i < n ; ++ i ) { ts [ i ] = new int [] { tasks [ i ][ 0 ], tasks [ i ][ 1 ], i }; } Arrays . sort ( ts , ( a , b ) -> a [ 0 ] - b [ 0 ]); int [] ans = new int [ n ]; PriorityQueue < int []> q = new PriorityQueue <>(( a , b ) -> a [ 0 ] == b [ 0 ] ? a [ 1 ] - b [ 1 ] : a [ 0 ] - b [ 0 ]); int i = 0 , t = 0 , k = 0 ; while (! q . isEmpty () || i < n ) { if ( q . isEmpty ()) { t = Math . max ( t , ts [ i ][ 0 ]); } while ( i < n && ts [ i ][ 0 ] <= t ) { q . offer ( new int [] { ts [ i ][ 1 ], ts [ i ][ 2 ]}); ++ i ; } var p = q . poll (); ans [ k ++] = p [ 1 ]; t += p [ 0 ]; } return ans ; } }class Solution { public: vector < int > getOrder ( vector < vector < int >>& tasks ) { int n = 0 ; for ( auto & task : tasks ) task . push_back ( n ++ ); sort ( tasks . begin (), tasks . end ()); using pii = pair < int , int > ; priority_queue < pii , vector < pii > , greater < pii >> q ; int i = 0 ; long long t = 0 ; vector < int > ans ; while ( ! q . empty () || i < n ) { if ( q . empty ()) t = max ( t , ( long long ) tasks [ i ][ 0 ]); while ( i < n && tasks [ i ][ 0 ] <= t ) { q . push ({ tasks [ i ][ 1 ], tasks [ i ][ 2 ]}); ++ i ; } auto [ pt , j ] = q . top (); q . pop (); ans . push_back ( j ); t += pt ; } return ans ; } };class Solution : def getOrder ( self , tasks : List [ List [ int ]]) -> List [ int ]: for i , task in enumerate ( tasks ): task . append ( i ) tasks . sort () ans = [] q = [] n = len ( tasks ) i = t = 0 while q or i < n : if not q : t = max ( t , tasks [ i ][ 0 ]) while i < n and tasks [ i ][ 0 ] <= t : heappush ( q , ( tasks [ i ][ 1 ], tasks [ i ][ 2 ])) i += 1 pt , j = heappop ( q ) ans . append ( j ) t += pt return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.