Greedy
Use a max-heap of task frequencies with a cooldown window. Always execute the most frequent available task. Fill idle slots when no task is available.
Imagine you are a CPU scheduler. You have a list of tasks represented by characters, say ["A","A","A","B","B","B"], and a cooldown constraint n. Between any two executions of the same task, at least n intervals must pass. You can insert idle intervals if needed. The goal: find the minimum number of intervals required to execute all tasks.
This is LeetCode 621, Task Scheduler, and it is one of the best problems for building intuition about greedy scheduling. The key insight is deceptively simple: the most frequent task dictates everything.
Let's work through the example. We have tasks ["A","A","A","B","B","B"] with n = 2. Task A appears 3 times and task B appears 3 times.
Start by focusing only on A, the most frequent task (tied with B, but pick either). With a cooldown of 2 between consecutive A's, the earliest possible schedule for A alone looks like this:
A _ _ A _ _ AEach A is followed by n = 2 slots before the next A can run. Think of this as (max_freq - 1) frames, each of width (n + 1). We have 2 frames of width 3, plus the final A at the end.
Now fill in B's. B also has frequency 3, so we slot one B into each frame and one at the tail:
A B _ A B _ A BThat's 8 intervals total. No idle time was needed because we had enough distinct tasks to fill the gaps.
This visual reasoning leads directly to a formula.
Count the frequency of every task. Let max_freq be the highest frequency, and let max_count be the number of tasks that share that highest frequency. The minimum intervals are:
(max_freq - 1) * (n + 1) + max_countWhy does this work? The expression (max_freq - 1) * (n + 1) accounts for the first max_freq - 1 frames, each containing one execution of the dominant task plus n cooldown slots. The + max_count tacks on the final execution of every task that appears max_freq times.
In our example: max_freq = 3, max_count = 2 (both A and B appear 3 times), n = 2.
(3 - 1) * (2 + 1) + 2 = 2 * 3 + 2 = 8That matches the 8 intervals we found by hand.
max(formula, len(tasks))The formula can undercount when there are many distinct tasks. Consider tasks ["A","A","A","B","B","B","C","C","C","D","D","E","E","F","G"] with n = 2. Here max_freq = 3, max_count = 3, so the formula gives (3-1)*(2+1) + 3 = 9. But we have 15 tasks! You cannot execute 15 tasks in 9 intervals.
When you have enough variety to fill every gap and then some, no idle time is needed at all. You just run tasks back to back. The answer is simply the total number of tasks. So the final answer is:
max((max_freq - 1) * (n + 1) + max_count, len(tasks))The first term wins when idle slots are inevitable; the second term wins when the schedule is packed with distinct work.
Tasks: ["A","A","B","B","C","C","D","D"], n = 1.
Frequencies: A=2, B=2, C=2, D=2. So max_freq = 2, max_count = 4.
Formula: (2-1) * (1+1) + 4 = 2 + 4 = 6. But len(tasks) = 8. The answer is max(6, 8) = 8.
One valid schedule: A B C D A B C D. Every cooldown is satisfied, and there are zero idle slots. The sheer number of distinct tasks eliminated any need to wait.
from collections import Counter
def leastInterval(tasks, n):
freq = Counter(tasks)
max_freq = max(freq.values())
max_count = sum(1 for f in freq.values() if f == max_freq)
return max(len(tasks), (max_freq - 1) * (n + 1) + max_count)This runs in time and space where k is the number of unique tasks (at most 26 for uppercase letters). It is the optimal approach when you only need the count of intervals.
Sometimes the problem asks not just how many intervals, but what order the tasks run in. The formula cannot tell you that. For this, we simulate the schedule with a max-heap.
The strategy is greedy: at each time step, pick the task with the highest remaining frequency that is not on cooldown. This is optimal because scheduling the most frequent task as early as possible minimizes future idle slots.
The data structures:
(-remaining_count, task_id) for every task still needing execution.(-remaining_count, available_at_time) for tasks that were just executed and are waiting for their cooldown to expire.At each time step:
available_at_time equals the current time, pop it and push it back onto the heap.available_at_time = current_time + n + 1.from collections import Counter, deque
import heapq
def leastInterval(tasks, n):
freq = Counter(tasks)
heap = [-f for f in freq.values()]
heapq.heapify(heap)
cooldown = deque() # entries: (neg_count, available_at)
time = 0
while heap or cooldown:
time += 1
# Return tasks from cooldown when ready
if cooldown and cooldown[0][1] == time:
heapq.heappush(heap, cooldown.popleft()[0])
if heap:
neg_count = heapq.heappop(heap)
remaining = neg_count + 1 # executed one (remember counts are negative)
if remaining < 0: # still has more executions
cooldown.append((remaining, time + n + 1))
# else: idle interval
return timeLet's trace through ["A","A","A","B","B","B"] with n = 2:
| Time | Heap | Cooldown | Action |
|---|---|---|---|
| 1 | [-3, -3] | [] | Pop A (freq 3), execute. Cooldown: (A, -2, t=4) |
| 2 | [-3] | [(A,-2,4)] | Pop B (freq 3), execute. Cooldown: (B,-2,4), wait... |
| 3 | [] | [(A,-2,4),(B,-2,5)] | Heap empty, idle |
| 4 | [-2] | [(B,-2,5)] | A returns. Pop A, execute. |
| 5 | [-2] | [(A,-1,7)] | B returns. Pop B, execute. |
| 6 | [] | [(A,-1,7),(B,-1,8)] | Idle |
| 7 | [-1] | [(B,-1,8)] | A returns. Pop A, execute. Done with A. |
| 8 | [-1] | [] | B returns. Pop B, execute. Done with B. |
Total: 8 intervals. Same answer as the formula, but now we know the order: A B idle A B idle A B.
The heap approach runs in time, where n is the total number of intervals (including idle) and k is the number of unique tasks. Each heap operation is , and we do at most one per time step.
A closely related problem is LeetCode 767, Reorganize String, which applies the same core idea. Given a string like "aab", rearrange it so that no two adjacent characters are the same. Return "" if impossible.
This is task scheduling with n = 1: every character must have at least one different character between consecutive occurrences. The feasibility check is: the most frequent character can appear at most ceil(len(s) / 2) times. If it exceeds that, there is no valid arrangement.
from collections import Counter
import heapq
def reorganizeString(s):
freq = Counter(s)
max_freq = max(freq.values())
if max_freq > (len(s) + 1) // 2:
return ""
heap = [(-count, char) for char, count in freq.items()]
heapq.heapify(heap)
result = []
prev_count, prev_char = 0, ""
while heap:
neg_count, char = heapq.heappop(heap)
# Re-add the previously used character if it still has remaining count
if prev_count < 0:
heapq.heappush(heap, (prev_count, prev_char))
result.append(char)
prev_count = neg_count + 1 # used one occurrence
prev_char = char
return "".join(result)The trick here is holding the previously-used character aside for one round so it cannot be picked again immediately. This is equivalent to a cooldown queue of size 1.
Why is it always correct to greedily pick the most frequent available task? Consider the alternative: suppose at some point you pick a less frequent task when a more frequent one is available. The more frequent task still needs all its executions, and delaying it only pushes those executions into a tighter window later. This can only increase idle time, never decrease it.
More formally, the most frequent task creates the "frame" that everything else must fit into. If you have a task with frequency f, you need at least (f - 1) * n cooldown slots dedicated to it. Executing it as early as possible at each opportunity gives other tasks the maximum room to fill those cooldown slots.
This greedy choice, always picking the highest-frequency available task, is what the max-heap naturally implements. And the formula is the closed-form consequence of that same logic: the dominant frequency determines the skeleton, and everything else either fills the gaps or overflows them.
Formula approach:
Heap approach:
For most interview problems, the formula approach is preferred because it is simpler and faster. Use the heap when you need the actual task ordering or when the problem has additional constraints (like task dependencies) that the formula cannot handle.
The formula is the fast path; the heap is the general-purpose engine. Both rest on the same foundation: the most frequent task defines the structure, and everything else fills in around it.
You're probably looking at task CPU scheduling when:
Common templates:
Practice Problems (9)