621. Task Scheduler - Explanation
Description
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
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(m) time and O(1) space, where m is the size of the input array.
Hint 1
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?
Hint 2
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.
Hint 3
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?
Hint 4
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.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Hash Maps / Frequency Counting - Used to count occurrences of each task type
- Greedy Algorithms - The optimal solution prioritizes the most frequent tasks first
- Heap / Priority Queue - The efficient solution uses a max-heap to always select the highest-frequency task
- Queue Data Structure - Used to track tasks in cooldown before they can be re-executed
1. Brute Force
Intuition
We simulate the CPU one time unit at a time.
At every step, we look at all remaining tasks and pick:
- A task that is not in the cooldown (not executed in the last
ntime units), - Among those, the one with the highest remaining count.
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.
Algorithm
- Count how many times each task appears (e.g., using an array of size 26 or a map).
- Build a list/array of pairs:
(remaining_count, task_id)for all tasks with count > 0. - Maintain:
time= 0 (total time elapsed),- A list/array
processedstoring, for each time unit, which task was executed (or an indicator like-1for idle).
- While there are still tasks left in the list:
- Set
best_task_index = -1. - For each task in the list:
- Check if this task was not executed in any of the last
ntime units
(i.e., itstask_iddoes not appear inprocessed[max(0, time − n) .. time − 1]). - If it is allowed and either:
best_task_index == -1, or- its
remaining_countis greater than the current best'sremaining_count,
then updatebest_task_indexto this task.
- Check if this task was not executed in any of the last
- Increase
timeby 1 (we are filling one time unit). - If
best_task_index != -1:- Execute that task in this time unit:
- Decrease its
remaining_countby 1. - If its
remaining_countbecomes 0, remove it from the list. - Append its
task_idtoprocessed.
Otherwise:
- Decrease its
- No valid task can be executed (all are in cooldown), so:
- Append an idle marker (e.g.,
-1) toprocessed.
- Append an idle marker (e.g.,
- Execute that task in this time unit:
- Set
- When the list of tasks becomes empty, return
timeas 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 timeTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the time to process given tasks and is the cooldown time.
2. Max-Heap
Intuition
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:
- Keep a max-heap of tasks by their remaining count (most frequent on top).
- At each time unit, we take the most frequent available task and run it.
- After running a task, it goes into a cooldown queue with the time when it will be available again (current time +
n). - When a task’s cooldown finishes, we push it back into the heap so it can be scheduled again.
- If the heap is empty but some tasks are still in cooldown, we can jump the current time forward to the next time when a task becomes available.
This way we always use the CPU as efficiently as possible while respecting the cooldown.
Algorithm
- Count how many times each task appears.
- Build a max-heap where each entry is "remaining count" of a task (the higher the count, the higher its priority).
- Create an empty queue (FIFO) to store pairs:
(remaining_count_after_running, next_available_time). - Set
time = 0. - While the heap is not empty or the cooldown queue is not empty:
- Increment
timeby 1. - If the heap is not empty:
- Pop the task with the largest remaining count.
- "Run" it once:
remaining_count -= 1. - If
remaining_count > 0, push(remaining_count, time + n)into the cooldown queue (it can be used again afternunits).
- Check the front of the cooldown queue:
- While the task at the front has
next_available_time == time,
remove it from the queue and push itsremaining_countback into the max-heap.
- While the task at the front has
- (Optional optimization)
- If the heap is empty and the cooldown queue is not empty:
- Let
next_timebe thenext_available_timeof the front element in the cooldown queue. - Set
time = next_time(fast-forward), then process step 3 again for that time.
- Let
- If the heap is empty and the cooldown queue is not empty:
- Increment
- When both the heap and cooldown queue are empty, return
timeas 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 timeTime & Space Complexity
- Time complexity:
- Space complexity: since we have at most different characters.
Where is the number of tasks.
3. Greedy
Intuition
Instead of simulating the whole schedule, we can think in terms of slots:
Let
maxfbe the maximum frequency of any task (e.g., ifAappears 5 times,B3 times, thenmaxf = 5).Imagine placing all copies of the most frequent task in a row:
A _ _ A _ _ A _ _ A _ _ AThere are
maxf - 1gaps between these most frequent tasks.Each gap must be at least size
nto satisfy the cooldown.So initial idle slots needed =
(maxf - 1) * n.
Now, we try to fill these idle slots using other tasks:
- For each other task with count
c, it can fill up tomin(c, maxf - 1)of these gaps (because there are onlymaxf - 1gaps). - Subtract this filled amount from the idle slots.
- After considering all tasks, if
idleis still positive, we must add those idle slots to the total time. - If
idlebecomes zero or negative, it means all gaps are already filled (or over-filled) by tasks, so no extra idle time is needed.
Finally:
- Total time =
len(tasks)(each task takes 1 unit) +max(0, idle)(extra gaps we couldn’t fill).
Algorithm
- Count how many times each task appears (frequency array or map).
- Find
maxf= maximum frequency among all tasks. - Compute initial idle slots:
idle = (maxf - 1) * n. - For each other task with count
c:- Decrease
idlebymin(maxf - 1, c)(this task helps fill gaps).
- Decrease
- If
idleis still positive, total time =len(tasks) + idle. - If
idleis zero or negative, total time =len(tasks)(no extra idle time needed). - Return this total 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
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)Time & Space Complexity
- Time complexity:
- Space complexity: since we have at most different characters.
Where is the number of tasks.
4. Math
Intuition
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)
Algorithm
- Count how many times each task appears.
- Find
maxf= the highest task frequency. - Count how many tasks have this highest frequency →
maxCount. - Compute:
time = (maxf - 1) * (n + 1) + maxCount - Return:
max(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)Time & Space Complexity
- Time complexity:
- Space complexity: since we have at most different characters.
Where is the number of tasks.
Common Pitfalls
Not Prioritizing the Most Frequent Task
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.
Incorrect Cooldown Tracking
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.
Forgetting to Count Tasks with Maximum Frequency
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.
Sign in to join the discussion