You are given an array of CPU tasks tasks, where tasks[i] is an uppercase english character from A to Z. You are also given an integer n.
Each CPU cycle allows the completion of a single task, and tasks may be completed in any order.
The only constraint is that identical tasks must be separated by at least n CPU cycles, to cooldown the CPU.
Return the minimum number of CPU cycles required to complete all tasks.
Example 1:
Input: tasks = ["X","X","Y","Y"], n = 2
Output: 5Explanation: A possible sequence is: X -> Y -> idle -> X -> Y.
Example 2:
Input: tasks = ["A","A","A","B","C"], n = 3
Output: 9Explanation: A possible sequence is: A -> B -> C -> Idle -> A -> Idle -> Idle -> Idle -> A.
Constraints:
1 <= tasks.length <= 10000 <= n <= 100
You should aim for a solution with O(m) time and O(1) space, where m is the size of the input array.
There are at most 26 different tasks, represented by A through Z. It is more efficient to count the frequency of each task and store it in a hash map or an array of size 26. Can you think of a way to determine which task should be processed first?
We should always process the most frequent task first. After selecting the most frequent task, we must ensure that it is not processed again until after n seconds, due to the cooldown condition. Can you think of an efficient way to select the most frequent task and enforce the cooldown? Perhaps you could use a data structure that allows for O(1) time to retrieve the maximum element and another data structure to cooldown the processed tasks.
We can use a Max-Heap to efficiently retrieve the most frequent task at any given instance. However, to enforce the cooldown period, we must temporarily hold off from reinserting the processed task into the heap. This is where a queue data structure comes in handy. It helps maintain the order of processed tasks. Can you implement this?
We start by calculating the frequency of each task and initialize a variable time to track the total processing time. The task frequencies are inserted into a Max-Heap. We also use a queue to store tasks along with the time they become available after the cooldown. At each step, if the Max-Heap is empty, we update time to match the next available task in the queue, covering idle time. Otherwise, we process the most frequent task from the heap, decrement its frequency, and if it's still valid, add it back to the queue with its next available time. If the task at the front of the queue becomes available, we pop it and reinsert it into the heap.
Before attempting this problem, you should be comfortable with:
We simulate the CPU one time unit at a time.
At every step, we look at all remaining tasks and pick:
n time units),If no such task is available, the CPU idles for that time unit.
We repeat this until all tasks are finished.
This is a direct, brute-force simulation: very easy to understand, but not efficient.
(remaining_count, task_id) for all tasks with count > 0.time = 0 (total time elapsed),processed storing, for each time unit, which task was executed (or an indicator like -1 for idle).best_task_index = -1.n time unitstask_id does not appear in processed[max(0, time − n) .. time − 1]).best_task_index == -1, orremaining_count is greater than the current best's remaining_count,best_task_index to this task.time by 1 (we are filling one time unit).best_task_index != -1:remaining_count by 1.remaining_count becomes 0, remove it from the list.task_id to processed.-1) to processed.time as the total minimum time required.class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
count = [0] * 26
for task in tasks:
count[ord(task) - ord('A')] += 1
arr = []
for i in range(26):
if count[i] > 0:
arr.append([count[i], i])
time = 0
processed = []
while arr:
maxi = -1
for i in range(len(arr)):
if all(processed[j] != arr[i][1] for j in range(max(0, time - n), time)):
if maxi == -1 or arr[maxi][0] < arr[i][0]:
maxi = i
time += 1
cur = -1
if maxi != -1:
cur = arr[maxi][1]
arr[maxi][0] -= 1
if arr[maxi][0] == 0:
arr.pop(maxi)
processed.append(cur)
return timeWhere is the time to process given tasks and is the cooldown time.
We always want to run the task that still has the most remaining occurrences, because those are the hardest to fit into the schedule (they need more slots with cooldown gaps).
So we:
n).This way we always use the CPU as efficiently as possible while respecting the cooldown.
(remaining_count_after_running, next_available_time).time = 0.time by 1.remaining_count -= 1.remaining_count > 0, push (remaining_count, time + n) into the cooldown queue (it can be used again after n units).next_available_time == time,remaining_count back into the max-heap.next_time be the next_available_time of the front element in the cooldown queue.time = next_time (fast-forward), then process step 3 again for that time.time as the minimum time required to finish all tasks.class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
count = Counter(tasks)
maxHeap = [-cnt for cnt in count.values()]
heapq.heapify(maxHeap)
time = 0
q = deque() # pairs of [-cnt, idleTime]
while maxHeap or q:
time += 1
if not maxHeap:
time = q[0][1]
else:
cnt = 1 + heapq.heappop(maxHeap)
if cnt:
q.append([cnt, time + n])
if q and q[0][1] == time:
heapq.heappush(maxHeap, q.popleft()[0])
return timeWhere is the number of tasks.
Instead of simulating the whole schedule, we can think in terms of slots:
Let maxf be the maximum frequency of any task (e.g., if A appears 5 times, B 3 times, then maxf = 5).
Imagine placing all copies of the most frequent task in a row:
A _ _ A _ _ A _ _ A _ _ A
There are maxf - 1 gaps between these most frequent tasks.
Each gap must be at least size n to satisfy the cooldown.
So initial idle slots needed = (maxf - 1) * n.
Now, we try to fill these idle slots using other tasks:
c, it can fill up to min(c, maxf - 1) of these gaps (because there are only maxf - 1 gaps).idle is still positive, we must add those idle slots to the total time.idle becomes zero or negative, it means all gaps are already filled (or over-filled) by tasks, so no extra idle time is needed.Finally:
len(tasks) (each task takes 1 unit) + max(0, idle) (extra gaps we couldn’t fill).maxf = maximum frequency among all tasks.idle = (maxf - 1) * n.c:idle by min(maxf - 1, c) (this task helps fill gaps).idle is still positive, total time = len(tasks) + idle.idle is zero or negative, total time = len(tasks) (no extra idle time needed).class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
count = [0] * 26
for task in tasks:
count[ord(task) - ord('A')] += 1
count.sort()
maxf = count[25]
idle = (maxf - 1) * n
for i in range(24, -1, -1):
idle -= min(maxf - 1, count[i])
return max(0, idle) + len(tasks)Where is the number of tasks.
The task with the highest frequency determines the minimum needed structure of the schedule.
If a task appears maxf times, these copies must be at least n units apart.
This creates (maxf - 1) "gaps", and each gap must have a length of (n + 1) slots (the task itself + n cooldowns).
If multiple tasks share this maximum frequency (maxCount tasks), they all occupy the final row of the structure.
So the minimal time required to schedule all tasks without violating cooldown rules is: time = (maxf - 1) * (n + 1) + maxCount
However, if the number of tasks is larger than this calculated time, then simply performing all tasks takes longer.
Thus, the actual answer must be: max(len(tasks), time)
maxf = the highest task frequency.maxCount.time = (maxf - 1) * (n + 1) + maxCountmax(len(tasks), time)class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
count = [0] * 26
for task in tasks:
count[ord(task) - ord('A')] += 1
maxf = max(count)
maxCount = 0
for i in count:
maxCount += 1 if i == maxf else 0
time = (maxf - 1) * (n + 1) + maxCount
return max(len(tasks), time)Where is the number of tasks.
A greedy approach must always pick the task with the highest remaining count that is not in cooldown. Picking tasks arbitrarily or in the order they appear leads to suboptimal schedules with more idle time than necessary.
When using a heap with a cooldown queue, the task should become available at time current_time + n, not current_time + n + 1. Off-by-one errors in cooldown calculations are common and result in either too many idle slots or cooldown violations.
In the math-based solution, you must count how many tasks share the maximum frequency (maxCount). Using just 1 instead of maxCount in the formula (maxf - 1) * (n + 1) + maxCount underestimates the required time when multiple tasks have the same highest frequency.