Algorithm

Task Scheduler

Heaps & Priority Queues Pattern

Task Scheduler

You are given an array of CPU tasks, each labelled with an uppercase letter, and an integer n. The CPU runs exactly one interval at a time. Two tasks with the same label must be separated by at least n intervals — between one A and the next A there must be n slots holding something else.

Each of those slots may hold a different task or be left idle, and idle intervals count toward the total. You choose the order freely; the order given in the input carries no meaning. Return the minimum number of intervals needed to run every task.

CONSTRAINTS
  • 1 <= tasks.length <= 10⁴
  • tasks[i] is an uppercase English letter (so at most 26 distinct labels)
  • 0 <= n <= 100
  • Idle intervals count toward the returned total.
  • You return the number of intervals only — not the schedule itself.
EXAMPLE 1
Input: tasks = [A,A,A,B,B,B], n = 2
Output: 8
A B idle A B idle A B fills 8 intervals. Each A is 3 intervals after the previous A, satisfying the gap of 2, and likewise for B. With only two labels there is nothing to put in the third slot of each round, so an idle is unavoidable — and no arrangement does better than 8.
EXAMPLE 2
Input: tasks = [A,A,A,B,B,B], n = 0
Output: 6
A gap of 0 means no separation is required at all, so tasks run back to back with no idles and the answer is simply the number of tasks.
EXAMPLE 3
Input: tasks = [A,A,A,A,B,C], n = 3
Output: 13
A occurs 4 times and each pair of As needs 3 intervals between them, so the As alone span 4 + 3 × 3 = 13 intervals from the first to the last. Only B and C exist to fill those 9 gap slots, leaving 7 of them idle. The total is set entirely by A — the other tasks fit inside the gaps and add nothing.
EXAMPLE 4
Input: tasks = [A,B,C,D,E,A,B,C,D,E], n = 4
Output: 10
Five distinct labels each occur twice. Running A B C D E A B C D E puts 4 intervals between the two copies of every label, exactly meeting the requirement. Nothing is idle, so the answer equals the number of tasks.
Does the order of tasks in the input array constrain my schedule?
No. The array is a multiset of work to be done, not a queue. You may run them in any order, which is exactly what makes minimising possible.
Is the gap n the number of intervals between two identical tasks, or the distance between their positions?
The number of intervals in between. With n = 2, an A at interval 0 permits the next A at interval 3 — positions differ by n+1. Getting this off by one is the single most common way to fail this problem, so confirm it.
During an A's cooldown, may I run any other task, including a task also on cooldown?
You may run any task whose own cooldown has expired. The restriction is per-label, so an A cooling down never blocks B — but if B also ran recently, B's own gap must still be respected.
Do I return the total intervals or the number of idles?
The total intervals, idles included. If the schedule needs no idles, the answer equals tasks.length.

Start with a question that reframes everything: which task decides how long the schedule must be? Rare tasks are easy — one A somewhere and one B somewhere have no gap to respect. Difficulty comes from repetition, because every repeat drags a mandatory gap behind it. So the task with the highest count is the one that stretches the timeline, and everything else is filler competing for the space it leaves behind.

A first attempt, and why it needs care

The tempting move is to simulate the CPU interval by interval: at each tick, look through all labels for one that is not cooling down, run it, and record when it becomes available again. If nothing is available, insert an idle.

python
def least_interval(tasks, n):
    remaining = Counter(tasks)
    ready_at = {label: 0 for label in remaining}
    time = 0
    while any(remaining.values()):
        pick = None
        for label, count in remaining.items():
            if count > 0 and ready_at[label] <= time:
                pick = label            # <-- which one should we pick?
                break
        if pick:
            remaining[pick] -= 1
            ready_at[pick] = time + n + 1
        time += 1
    return time

The loop is honest but the marked line is wrong, and the reason is the heart of the problem. Picking any available task is not good enough. Suppose A remains 5 times and B once, with n = 2. Running B first wastes the only slot where A could have gone, and A's forced gap now pushes the whole tail of the schedule further out. B was never the bottleneck — it could have been slotted in anywhere later — while every interval that A is not running is an interval where the finish line moves.

That gives the greedy rule: at each step run the task with the most remaining copies, among those not on cooldown. The intuition is scheduling scarcity — deal with the constraint that will run out of room, and let flexible work fill in around it. And "give me the largest remaining count, repeatedly, while those counts keep changing" is the definition of a max-heap's job.

The heap as a scheduler

This is a different use of the heap from Kth Largest and Top K Frequent. There, the heap was a fixed-size filter that discarded losers permanently. Here nothing is discarded — a task is popped, executed once, and then pushed back with a reduced count to compete again later. The heap is being used for what a priority queue really is: repeatedly serving whoever currently deserves it most, in a world where priorities change after every service.

Simulating tick by tick with a heap works, but there is a cleaner shape. Process the timeline one round at a time, where a round is n+1 consecutive intervals — precisely the span from starting a task to being allowed to start it again. Within one round, no label may appear twice. So the best possible round is: pop up to n+1 distinct labels, highest counts first, run each once, and set their decremented counts aside. Any slot in the round you could not fill is an idle.

python
import heapq
from collections import Counter

def least_interval(tasks, n):
    counts = Counter(tasks)
    heap = [-c for c in counts.values()]   # negate: Python has only a min-heap
    heapq.heapify(heap)
    time = 0

    while heap:
        carried = []
        for _ in range(n + 1):             # one round of n+1 slots
            if heap:
                count = -heapq.heappop(heap) - 1
                if count > 0:
                    carried.append(-count)
            time += 1
            if not heap and not carried:
                break                      # final round: stop at the last real task
        for c in carried:
            heapq.heappush(heap, c)
    return time

Two details carry all the weight. Python's heapq is a min-heap only, so storing negated counts turns it into a max-heap — the most frequent task has the most negative key and therefore sits at the root. And the early break matters for correctness, not speed: idles are only needed if work still remains afterwards. In the very last round you stop counting the moment the work runs out, because trailing idles are never part of a minimal schedule.

Cost: each task copy is popped and pushed once, and the heap holds at most 26 entries, so it is O(total tasks × log 26), effectively O(N).

Worked example:tasks = [A,A,A,A,B,C], n = 3

Counts: A four times, B once, C once. Rounds are 4 intervals long.

- Round 1. Pop A (4 left, becomes 3), pop B (1 left, becomes 0 and is not carried), pop C (likewise done), then the heap is empty but A is carried, so the fourth slot is an idle. Time is 4. Timeline: A B C idle.
- Round 2. Only A is left. Pop A (3 becomes 2), and the remaining three slots idle because A itself may not repeat inside a round. Time is 8. Timeline: A B C idle | A idle idle idle.
- Round 3. Pop A (2 becomes 1), three more idles. Time is 12.
- Round 4. Pop A (1 becomes 0, nothing carried). After that slot the heap is empty and nothing is carried, so the break fires and the round ends immediately rather than padding out three useless intervals. Time is 13.

Answer 13, and the schedule is A B C idle A idle idle idle A idle idle idle A. The two flexible tasks did nothing to shorten it; A's four copies with gaps of 3 fixed the length before B and C were even considered.

The formula, and why it is true

The simulation makes the structure visible, and once you see it the whole thing collapses into arithmetic. Let f be the highest count and let c be how many labels tie at that count.

Fix on a most-frequent label. Its f copies split the timeline into f-1 gaps, each of which must be at least n intervals wide. So that label alone forces a skeleton of f-1 blocks, each holding 1 run plus n following slots — that is (f-1) × (n+1) intervals — and then the final copy at the end. If c labels share the top count, all of them must appear in the last block, so the tail is c intervals rather than 1:

text
minimum = (f - 1) * (n + 1) + c

Every other task has a count of at most f, so each of them can be dropped into the gap slots without ever needing two copies inside the same block — meaning they never extend the skeleton. They only fill it.

Unless there is not enough room. If the filler tasks outnumber the empty slots, the extras have to go somewhere, and the schedule simply grows to hold everything. But in that situation the gaps are all full, no idles exist at all, and the answer is exactly the number of tasks. So:

python
def least_interval(tasks, n):
    counts = Counter(tasks)
    f = max(counts.values())
    c = sum(1 for v in counts.values() if v == f)
    return max(len(tasks), (f - 1) * (n + 1) + c)

Check it against the examples. For [A,A,A,A,B,C] with n = 3: f = 4, c = 1, so the skeleton is 3 × 4 + 1 = 13, and the task count 6 is smaller, giving 13. For [A,B,C,D,E,A,B,C,D,E] with n = 4: f = 2, c = 5, so the skeleton is 1 × 5 + 5 = 10, tied with the task count 10, giving 10. For [A,A,A,B,B,B] with n = 2: f = 3, c = 2, so 2 × 3 + 2 = 8 against a task count of 6, giving 8.

This is O(N) time and O(26) space, and it never builds a schedule at all.

What to actually say in an interview

Reach for the heap first. It is the approach you can derive live from the greedy rule, it is obviously correct, and it survives the follow-up questions that break the formula — what if each task has a different cooldown, or print the actual schedule, or tasks arrive over time. The formula answers exactly one question and answers it beautifully, but it computes a number rather than a plan, so any variant that asks for the plan makes it useless.

The transferable idea is the shift in what a heap is for. In the earlier problems it was a filter over a fixed dataset. Here it is a live queue whose priorities change as the simulation runs, with elements leaving and returning with updated keys. That pop-modify-push loop is the shape of an enormous family of scheduling and simulation problems — CPU scheduling, meeting rooms, Dijkstra's shortest paths. Train the reflex: when a problem says repeatedly serve whoever is most urgent right now, and urgency changes after each service, the answer is a heap you push back into.

Interactive Strategy Visualization

Task Scheduler

Cooldown Logic mapped against temporal constraints (n=2)

Max-Heap (Frequency)
A
count 3
B
count 2
Cooldown Queue (Wait)
None cooling down
Execution Timeline
T=0
Phase Details
Start
Initially, tasks A (count 3) and B (count 2) are in the Max-Heap.

Dynamic Priority

This pattern combines a Max-Heap for priority and a Queue for time-based constraints. It's the standard way to solve complex scheduling with dependencies.

"Always schedule the task that has the most remaining count."

O(N × n) Tick-By-Tick Simulation
O(N log 26) Max-Heap Rounds
O(N) Counting Formula