621. Task Scheduler - Detailed Explanation

Free Coding Questions Catalog
Boost your coding skills with our essential coding questions catalog. Take a step towards a better tech career now!

Problem Statement

You are given an array of tasks represented by uppercase letters (for example, ["A","A","A","B","B","B"]) and a non-negative integer n that represents the cooldown period between two identical tasks. The CPU executes one task per time unit. When the same task is executed, there must be at least n intervals between two executions of that task. During intervals when no task is available, the CPU stays idle.

Objective:
Find the minimum number of intervals required to complete all the tasks while satisfying the cooldown constraint.

Example 1:

  • Input:
    Tasks = ["A","A","A","B","B","B"], n = 2
  • Output: 8
  • Explanation:
    One valid schedule is:
    A → B → idle → A → B → idle → A → B
    Each identical task is separated by at least 2 intervals. Total intervals = 8.

Example 2:

  • Input:
    Tasks = ["A","C","A","B","D","B"], n = 1
  • Output: 6
  • Explanation:
    A valid schedule is:
    A → C → A → B → D → B
    Total intervals = 6 since tasks can be executed consecutively when n = 1.

Hints to Solve the Problem

  1. Frequency Analysis:
    Count the frequency of each task. The task with the highest frequency will mostly determine the minimum schedule length because its occurrences must be spread apart with at least n intervals between them.

  2. Cooldown Tracking:
    For each task, keep track of the earliest time it can be scheduled next. If the current time is greater than or equal to that time, the task is available.

  3. Time Unit Simulation:
    Simulate the scheduling one time unit at a time. At each interval, determine all tasks that are available (i.e., not in their cooldown period) and choose one to execute—if multiple are available, you can choose the one with the highest remaining count. If no task is available, count that interval as idle.

  4. Termination:
    Continue the simulation until all tasks have been executed.

Approach 1: Brute Force Simulation

Idea: Simulate the task scheduling interval by interval, always executing an available task if possible and inserting idle periods when needed to respect the cooldown. The most frequent task largely determines the minimum timeline. We can imagine the schedule in terms of “frames” or buckets separated by cooldowns. If a task (say X) has the maximum frequency F, then we need to schedule those F occurrences with at least n intervals between them. This yields a preliminary length of (F - 1) * (n + 1) slots (each of the first F-1 occurrences of X followed by n cooldown slots). Finally, we place the last occurrence of X and any remaining tasks into the schedule. If other tasks are plentiful enough to fill all the idle slots, the total time will just be the number of tasks; otherwise, some idle slots remain.

How to simulate: We maintain a frequency map of tasks and a structure to track cooldowns (e.g., a queue of tasks waiting for their cooldown to expire). At each time step, we pick the task with the highest remaining count that is not in cooldown. If a task is executed, decrement its count and mark it as cooling down for the next n intervals. If no task can be executed (all remaining tasks are still cooling down), the CPU stays idle for that interval. This process repeats until all tasks are finished. This approach guarantees the cooldown constraint is satisfied by construction (we never schedule a task during its cooldown).

Optimally filling idle slots: The goal is to avoid idle time by filling cooldown gaps with other tasks whenever possible. In the bucket analogy, after placing the most frequent task X in its slots, we fill the idle slots in between with other tasks in order of frequency. If there are enough tasks to occupy all these slots, the schedule length will be simply len(tasks). If not, the remaining empty slots will be true idle intervals that add to the total time. The formula for the minimum intervals can be derived as:

[ \text{Intervals} = \max\{ \text{len(tasks)},\; (F - 1) \times (n + 1) + m \}, ]

where F is the max frequency of any task, and m is the number of tasks that have this maximum frequency. This formula accounts for the worst-case spacing required by the most frequent tasks and ensures we add any extra tasks with the same max frequency in the last frame.

Steps for Approach 1:

  • Count the frequency of each task using a hash map or array.
  • Identify the task with maximum frequency F. Let m be the number of tasks that occur F times.
  • Calculate the initial required slots: (F - 1) * (n + 1) + m. This represents F-1 full cycles of length n+1 plus a final cycle to accommodate the last occurrences of the most frequent tasks.
  • The minimum needed intervals is the larger of this value and the total number of tasks. (If total tasks are more, that means there were enough tasks to fill all idle gaps.)
  • Simulation alternative: Iterate through time units, each time picking an available task with the highest count to execute, or idling if none available. Use a queue to track tasks in cooldown and make them available after n intervals.

Python Implementation (Brute Force Simulation)

Python3
Python3

. . . .

Explanation: We increment time on each loop iteration to simulate each interval. We choose an available_task with the highest count (since at most 26 task types, scanning is efficient). After executing a task, we reduce its count and push it into a cooldown queue with the timestamp when it will be available again (current time + n + 1). If no task is available, the loop still does time += 1 (an idle interval). This simulation continues until all tasks are done. The result time is the total intervals used.

Java Implementation (Brute Force Simulation)

Java
Java

. . . .

Explanation: We use arrays for frequencies and next available times for each task (indexed by letters). At each time step (time increases each loop iteration), we scan for the task with the largest count that is not in cooldown (its nextAvailable time is <= current time). If found, we execute it: decrement its count and set its nextAvailable to time + n + 1 (meaning it cannot be run again until that future time). If none is available, that interval is idle. We continue until all tasks (remaining) are done. This approach directly simulates the scheduling, ensuring a valid order.

Approach 2: Using a Max Heap (Greedy Algorithm)

Idea: This approach uses a max-heap (priority queue) to always pick the task with the largest remaining frequency to execute next. By greedily scheduling the most frequent tasks first, we minimize idle time – we fill each interval with the most "critical" task available. We also use a queue to keep track of tasks during their cooldown period. The algorithm works as follows:

  1. Count frequencies: Use a counter for tasks. Initialize a max-heap with all task frequencies (highest frequency at top).

  2. Schedule in cycles: At each time step, attempt to schedule a task:

    • If the heap is not empty, pop the top task (the one with most remaining instances). Execute it in the current interval.
    • After executing, decrement its count. If it still has remaining occurrences, put it into a cooldown queue along with the time when it will be eligible to run again (current time + n).
    • If the heap is empty (no available tasks) but there are tasks in the cooldown queue that haven’t expired, the CPU idles for this interval.
  3. Cooldown management: Each interval, check the cooldown queue. If the task at the front of the queue has its cooldown expired (its ready time equals the current time), push it back into the heap, making it available again. This ensures tasks become available as soon as their cooldown is complete.

  4. Continue until done: Keep incrementing the time counter for each interval (whether a task ran or it was idle) until no tasks remain in either the heap or the cooldown queue.

This greedy approach efficiently schedules tasks and inserts idle intervals only when no task is available to run. By always using an available task if possible, it guarantees minimal idling. It essentially builds the schedule round by round, where each round can be thought of as up to (n+1) time units (one for the most frequent and n for others or idle).

Why it works: The max-heap ensures that at any point, you execute the task with the largest remaining count that isn’t cooling down. This is optimal because if a task has a lot of instances left, delaying it could force more idle slots later. Using the cooldown queue ensures we never violate the rule of waiting n intervals for the same task. If there are enough other tasks to fill the gap, they will be executed in the meantime; if not, the CPU will idle until the next task is ready.

Python Implementation (Max Heap)

Python3
Python3

. . . .

Explanation: We use a max heap (max_heap) to always pick the task with the largest remaining count. We increment time for each interval. If the heap is non-empty, we pop a task and simulate executing it (decrement its count). If it has more occurrences left, we push it into the cooldown deque with its next available time (current time + n). If the heap is empty (meaning no task available to run), that interval is idle (we still increment time). After that, we check the cooldown queue: if the task at the front has a ready time equal to the current time, we push it back into the heap (it has finished cooling down). The loop ends when there are no tasks left to schedule (both max_heap and cooldown are empty). The variable time will be the total intervals used.

Java Implementation (Max Heap)

Java
Java

. . . .

Explanation: We use a Java PriorityQueue (max-heap by using reverse order) to store task frequencies. A queue cooldown holds tasks that have been executed and are waiting for cooldown, stored as [readyTime, remainingCount]. In each loop iteration, we increment the time. If maxHeap is not empty, we poll the top frequency and simulate executing that task (decrement frequency). If it’s not yet finished, we add it to cooldown with its next readyTime (current time + n). Then we check if the task at front of cooldown is ready to be scheduled (its readyTime == current time); if so, we take it out and push its remaining count back into maxHeap (making it available for scheduling again). If the heap was empty (no task to run), the loop still increments time (idle interval). The process continues until all tasks are done. The time counter represents the total intervals used.

Complexity Analysis

  • Time Complexity (Approach 1: Simulation): In the simulation approach, each interval involves selecting a task from at most 26 possibilities, which is a constant factor. In the worst case, we might execute on the order of len(tasks) + idle intervals. The number of intervals is bounded by roughly (F - 1)*(n+1) + m (as derived above), which in the worst case could be slightly more than len(tasks) if many idles are needed. Therefore, the time complexity is about O(T), where T is the total intervals in the schedule. Since T is at most on the order of N * (n+1) in the worst scenario, and n is at most 100 (constant relative to N), this is effectively O(N). In more general terms, picking the max task each time could be seen as O(N * K) where K is the number of distinct task types (here K ≤ 26, a constant). Thus, the simulation is efficient for the given constraints. (If we naively considered trying all task order permutations, that would be factorial time, but our simulation is a greedy construction, not brute-force enumeration.)

  • Time Complexity (Approach 2: Max Heap): Building the initial heap takes O(K) time for K distinct tasks. Each task execution is a heap pop and potential push, each heap operation O(log K). We perform one heap pop per scheduled interval (total intervals T as above) and at most one heap push for each execution (and one push/pop for each cooldown release). Thus complexity is O(T * log K). Here K ≤ 26, so log K is small; in general if K grew with N, we say O(N log K). In the worst case (when n is large and there are idles), T might be greater than N, but as explained, T is bounded by N plus the idle slots needed. In the formula scenario, if idle slots are needed, T ≈ (F-1)*(n+1)+m. Even in the worst distribution, T is O(N + something) and K is at most N, so a loose bound is O(N log N) in the worst theoretical case. For our problem constraints, this is very manageable.

  • Space Complexity: Both approaches use additional data structures proportional to the number of distinct tasks. Approach 1 uses counters and possibly a cooldown queue of size at most K. Approach 2 uses a heap of size ≤ K and a cooldown queue of similar size. Thus both use O(K) extra space, which is O(26) = O(1) in this problem, or O(N) in the worst general scenario. Storing the input itself takes O(N). The output (just an integer count) is O(1).

In summary, both approaches run efficiently. The max-heap approach is more direct to implement using library structures and clearly O(N log K), while the simulation approach can achieve O(N) due to the fixed alphabet size.

Step-by-Step Walkthrough with Visuals

Let’s walk through Example 1 (Tasks = ["A","A","A","B","B","B"], n = 2) step by step to see how the scheduling works:

  • Task frequencies: A:3, B:3. The most frequent tasks are A and B, each with frequency 3. Here F = 3 and m = 2 (two tasks tie for max frequency).

  • Initial requirement: With F=3 and n=2, at minimum we need (F - 1)*(n + 1) + m = (3 - 1)*(2 + 1) + 2 = 2*3 + 2 = 8 intervals . This calculation suggests 8 is the least possible intervals given the cooldown constraint.

  • Schedule construction: We know we must separate the three A tasks by at least 2 intervals. Place them first with gaps:

    A _ _ A _ _ A
    

    Here underscores (_) represent required cooldown slots after each A (except the last). We have 3 A's which create 2 gaps (of length 2 each) between them.

  • Fill gaps with other tasks: Now we fill the idle slots _ with task B (the next most frequent). There are 3 B tasks to place. Fill the gaps one by one:

    A B _  A B _  A
    

    We placed two B tasks in the first two available gaps. One B remains. After filling the structured gaps, if tasks are still left, they will occupy additional slots at the end. Place the remaining B in the next interval after the last A:

    A B _  A B _  A B
    

    Now all B’s are placed. One gap (marked _) remains where no task was available to fill, which will be an idle interval.

  • Final schedule timeline: Reading the sequence with idle slots explicitly:

    A → B → idle → A → B → idle → A → B

    This is a valid execution order. Let’s label the time units:

    • Time 1: Execute A
    • Time 2: Execute B
    • Time 3: Idle (no A or B available, both are in cooldown)
    • Time 4: Execute A (A's cooldown complete)
    • Time 5: Execute B (B's cooldown complete)
    • Time 6: Idle (A and B both cooling down again)
    • Time 7: Execute A (last A, cooldown over)
    • Time 8: Execute B (last B, cooldown over)

    We used 8 intervals in total, matching the calculated minimum. There are at least 2 idle units between any two A’s (and similarly for B’s in this case), satisfying n=2.

  • Understanding the idle slots: In the above, idle slots occurred at Time 3 and Time 6 because at those moments, both A and B were still in their cooldown period (they had been used 2 intervals prior). As soon as the cooldown passed, we resumed executing the remaining tasks.

For Example 2 (Tasks = ["A","C","A","B","D","B"], n = 1): Task frequencies: A:2, B:2, C:1, D:1. The most frequent is 2, so minimum required slots = (2-1)*(1+1)+ (number of tasks with freq 2) = 1*2 + 2 = 4, but total tasks = 6, so answer will be 6 (since 6 > 4). In fact, we can schedule without any idle time because there are enough different tasks to intermix. One possible order (among others) is: A → B → A → C → B → D, which uses 6 intervals with no idles (and indeed each A or B has at least 1 interval separation). This matches the output of 6.

These examples illustrate how the scheduling fills in tasks and only idles when necessary. The first example needed idle periods due to the high frequency of A and B, whereas the second example’s diverse tasks allowed a continuous schedule.

Common Mistakes & Edge Cases

  • Assuming idle slots after the last task cycle: A common mistake is to always calculate (F - 1)*(n + 1) + m and return that, forgetting to take the max with the total number of tasks. If there are enough tasks to fill all idle gaps, the formula might underestimate. Always compare with len(tasks). If the formula result is smaller, it means tasks could be arranged with no idle time beyond the tasks themselves.

  • Not handling multiple max-frequency tasks: If more than one task has the maximum frequency, they all dictate the scheduling of the last part. For example, if A and B are both most frequent, the last frame will contain both A and B. The formula’s + m term (number of tasks with max frequency) accounts for this. Forgetting this can lead to scheduling one too few intervals.

  • Ignoring n = 0: When n = 0, there is no cooldown constraint, so tasks can be executed back-to-back. The answer in this case is simply len(tasks) (just do all tasks in any order with no idle). Failing to handle this as a special case could complicate logic or give wrong results (though the general formula also yields len(tasks) in this case, it’s straightforward to handle directly).

  • Improper cooldown tracking: When implementing, one might forget to release tasks from cooldown at the correct time. Make sure that a task becomes available exactly after n intervals have passed since its last execution – this was handled by tracking a ready time (last_time + n). Off-by-one errors can occur if this is mismanaged.

  • Using poor data structures: Scheduling by scanning for the next available task (as in Approach 1) is fine here because task types are limited. But if one tries a brute-force permutation or doesn’t use a heap when needed, the solution could become inefficient. Sticking to counting and greedy selection avoids exploring unnecessary orderings.

Edge cases to consider:

  • All tasks are the same (e.g. tasks = ["A","A",...,"A"]). Here you must place n idles between each task. The formula or simulation should handle this (it will produce (N-1)* (n+1) + 1 intervals for N tasks of type A).
  • Many tasks but n is 0 (already discussed; just return N).
  • n is large relative to number of tasks. If n is bigger than the number of other tasks, you will see multiple idle slots in a row. For instance, tasks = ["A","B"] with n=5 would schedule as A -> B -> idle -> idle -> idle -> idle -> idle (total 7 intervals) because after A and B there are no tasks to fill the long cooldown requirement for A’s next occurrence. The logic should correctly insert the required idles.
  • Cases where no idle time is needed at all (e.g. already well-distributed tasks or very small n). The algorithm should not introduce idle intervals when not required.
  • Rearrange String k Distance Apart: This is a classic similar problem where given a string and an integer k, you must rearrange the string such that the same characters are at least k apart. It’s essentially the same as the task scheduler problem but framed for arranging characters in a string. The approach using a max heap and queue is identical. (LeetCode 358 is an example of this problem, and it is directly related to Task Scheduler.)

  • Reorganize String: A related problem (LeetCode 767) asks to rearrange letters so that no two identical letters are adjacent. This is the special case of the above with k = 2 (or cooldown 1). The greedy heap solution is similar – if a valid arrangement exists, the algorithm will find it by always picking the most frequent remaining character that isn’t the same as the last placed.

  • Task Scheduler II (LeetCode 2365): A variation where tasks are given as an ordered list (stream of tasks in the given order, not freely reorderable). You must compute the time to finish tasks in that given order with a cooldown after executing the same task again. This changes the problem because you can’t reorder tasks arbitrarily; you may have to insert idle time when the same task appears sooner than cooldown allows. The solution pattern is different (often using a hash map to track the last index/time a task was seen and calculating waits).

  • CPU Scheduling with different processing times or weights: In real-world scheduling, tasks might have different durations or priorities (for example, some tasks might take multiple time units, or yield different profits if scheduled early). Those become different problems (like scheduling with deadlines, or weighted scheduling). For instance, Weighted Job Scheduling or Interval Scheduling problems involve selecting and ordering tasks based on start/end times or profits, which is solved with dynamic programming or greedy algorithms but not directly using cooldown logic.

  • Project scheduling and CPU round-robin: The concept of a cooldown is analogous to CPU time-slice scheduling where after using the CPU, a process might have to wait before getting another time slice. However, in round-robin scheduling, the “cooldown” is usually just giving other processes a turn, which is naturally handled by a queue. The idea of spacing out identical tasks is more specific to problems like the ones above.

TAGS
leetcode
CONTRIBUTOR
Design Gurus Team
-

GET YOUR FREE

Coding Questions Catalog

Design Gurus Newsletter - Latest from our Blog
Boost your coding skills with our essential coding questions catalog.
Take a step towards a better tech career now!
Explore Answers
What is ChatGPT AI?
Self-paced coding interview prep with interactive challenges
How hard is a Datadog interview?
Related Courses
Image
Grokking the Coding Interview: Patterns for Coding Questions
Grokking the Coding Interview Patterns in Java, Python, JS, C++, C#, and Go. The most comprehensive course with 476 Lessons.
Image
Grokking Modern AI Fundamentals
Master the fundamentals of AI today to lead the tech revolution of tomorrow.
Image
Grokking Data Structures & Algorithms for Coding Interviews
Unlock Coding Interview Success: Dive Deep into Data Structures and Algorithms.
Image
One-Stop Portal For Tech Interviews.
Copyright © 2025 Design Gurus, LLC. All rights reserved.
;