Taro Logo

Task Scheduler

Medium
5 views
a month ago

You are given an array of CPU tasks, each labeled with a letter from A to Z, and a number n. Each CPU interval can be idle or allow the completion of one task. Tasks can be completed in any order, but there's a constraint: there has to be a gap of at least n intervals between two tasks with the same label.

Return the minimum number of CPU intervals required to complete all tasks.

Example 1:

tasks = ["A","A","A","B","B","B"], n = 2

Output: 8

Explanation: A possible sequence is: A -> B -> idle -> A -> B -> idle -> A -> B.

After completing task A, you must wait two intervals before doing A again. The same applies to task B. In the 3rd interval, neither A nor B can be done, so you idle. By the 4th interval, you can do A again as 2 intervals have passed.

Example 2:

tasks = ["A","C","A","B","D","B"], n = 1

Output: 6

Explanation: A possible sequence is: A -> B -> C -> D -> A -> B.

With a cooling interval of 1, you can repeat a task after just one other task.

Example 3:

tasks = ["A","A","A", "B","B","B"], n = 3

Output: 10

Explanation: A possible sequence is: A -> B -> idle -> idle -> A -> B -> idle -> idle -> A -> B.

There are only two types of tasks, A and B, which need to be separated by 3 intervals. This leads to idling twice between repetitions of these tasks.

Constraints:

  • 1 <= tasks.length <= 10^4
  • tasks[i] is an uppercase English letter.
  • 0 <= n <= 100
Sample Answer
import heapq
from collections import Counter


def leastInterval(tasks, n):
    # Count the frequency of each task
    task_counts = Counter(tasks)
    
    # Create a max heap to store the frequencies
    max_heap = [-count for count in task_counts.values()]
    heapq.heapify(max_heap)
    
    time = 0
    queue = []  # (available_time, task_count)
    
    while max_heap or queue:
        time += 1
        
        # If there are tasks available, add them back to the max heap
        if queue and queue[0][0] == time:
            count = heapq.heappop(queue)[1]
            heapq.heappush(max_heap, count)
        
        # If there are tasks in the max heap, execute one
        if max_heap:
            count = heapq.heappop(max_heap)
            count += 1  # Decrement the count (since it's negative)
            
            if count != 0:
                available_time = time + n + 1
                heapq.heappush(queue, (available_time, count))
                
    return time

# Example usage
tasks1 = ["A","A","A","B","B","B"]
n1 = 2
print(leastInterval(tasks1, n1))  # Output: 8

tasks2 = ["A","C","A","B","D","B"]
n2 = 1
print(leastInterval(tasks2, n2))  # Output: 6

tasks3 = ["A","A","A", "B","B","B"]
n3 = 3
print(leastInterval(tasks3, n3))  # Output: 10

Explanation:

  1. Task Frequency Counting: The code starts by counting the frequency of each task using Counter(tasks). This is essential for understanding how many times each task needs to be executed.
  2. Max Heap Initialization: A max heap (max_heap) is created to store the frequencies of the tasks. The frequencies are negated because Python's heapq module implements a min heap. By negating the values, we can simulate a max heap.
  3. Main Loop: The while loop continues as long as there are tasks in the max_heap or tasks waiting in the queue. The time variable keeps track of the current CPU interval.
  4. Queue Management: The queue is used to keep track of tasks that are冷却 down. If a task's冷却 down period is over (i.e., queue[0][0] == time), it is added back to the max_heap.
  5. Task Execution: If there are tasks in the max_heap, the task with the highest frequency is executed. The count is decremented, and if the task still needs to be executed again, it is added to the queue with its available time.
  6. Return Time: Finally, the function returns the total time required to execute all tasks.

Example Walkthrough (tasks = ["A","A","A","B","B","B"], n = 2):

  1. Initial Counts: A: 3, B: 3
  2. Max Heap:
    • Initial: [-3, -3]
  3. Iterations:
    • time = 1: Execute A. max_heap = [-3], queue = [(4, -2)]
    • time = 2: Execute B. max_heap = [], queue = [(4, -2), (5, -2)]
    • time = 3: Idle. max_heap = [], queue = [(4, -2), (5, -2)]
    • time = 4: A becomes available. max_heap = [-2], queue = [(5, -2)]
    • time = 5: B becomes available. max_heap = [-2], queue = [(6, -1)]
    • time = 6: Execute A. max_heap = [-2], queue = [(6, -1)], new queue queue = [(6, -1), (9, -1)]
    • time = 7: Execute B. max_heap = [], queue = [(9, -1)]
    • time = 8: Execute A. max_heap = [], queue = []

Big(O) Run-time Analysis

  • Task Frequency Counting: O(T) where T is the number of tasks.
  • Heap Operations: In the worst-case scenario, each task is added and removed from the heap once. Heap operations (push and pop) take O(log K) time, where K is the number of unique tasks. Thus, this part takes O(T log K) time.
  • Total: The overall time complexity is dominated by the heap operations, so it's O(T log K). Since K is at most 26 (number of uppercase English letters), log K is effectively constant. Therefore, the time complexity can be approximated as O(T).

Big(O) Space Usage Analysis

  • Task Frequency Map: O(K), where K is the number of unique tasks (at most 26).
  • Max Heap: O(K), storing the counts of each unique task.
  • Queue: In the worst case, the queue can contain all unique tasks that are in their cooling period. This would be O(K).
  • Total: The overall space complexity is O(K). Again, since K is at most 26, this can be considered O(1) (constant space).

Edge Cases

  1. Empty Task List:
    • If the input tasks list is empty, the function should return 0.
  2. n = 0:
    • If n is 0, it means there is no cooling period. In this case, the minimum time is simply the number of tasks.
  3. Large n:
    • If n is very large compared to the number of tasks, there will be many idle slots. The algorithm correctly handles this by managing the queue appropriately.
  4. Tasks with Same Frequencies:
    • The algorithm handles tasks with the same frequencies correctly, as the max heap ensures that the task with the highest frequency is always processed first.
  5. All Tasks Are Unique:
    • If all tasks are unique, there will be no idle slots, and the minimum time will be equal to the number of tasks.