LeetCode #621 Medium

Task Scheduler

Given task labels and a cooldown n that must separate two runs of the same task, return the minimum number of CPU intervals needed to finish them all.

heapgreedycountingmath
Open on LeetCode ↗
02

Intuition

💡

Your first instinct is to simulate the clock: a max-heap of counts, pop up to n+1 distinct tasks each round, push back what still has work, tick, repeat. It passes, and it teaches you nothing, because it hides the fact that the answer was decided before the first tick. Look at only the most frequent task. If it appears m times, you are forced to lay it down m times with at least n slots between consecutive copies — that is a rigid skeleton of (m-1) rows, each n+1 wide, plus a final row holding however many tasks are tied at m. Every other task now has exactly one of two fates: it drops into an idle hole inside that skeleton, changing nothing, or the holes run out and it extends the line to the right, at which point the CPU never idles at all and the length is simply the number of tasks. So the answer is max(len(tasks), (m-1)*(n+1) + countOfTasksWithMaxFreq) — two lower bounds, and the larger one is always achievable. The invariant the greedy simulation is secretly maintaining is that the busiest task is scheduled as early as its cooldown permits, every single round.

03

Approach

1

Only the frequencies matter

The CPU may run tasks in any order, so the input array is not a sequence to preserve — it is a multiset. Count each label with a Counter in one pass. From that point on the labels themselves are irrelevant too; all that survives into the arithmetic is the maximum count and how many labels tie for it.

2

The busiest task lays down a fixed frame

If the maximum frequency is m, the m copies of that task cannot be packed tighter than one every n+1 slots. That gives (m-1) complete rows of width n+1. The final row holds the last copy of the busiest task and the last copy of every other task tied at frequency m, since those are forced into the same final position — hence the + countOfTasksWithMaxFreq rather than + 1.

3

Take the max against the task count

The frame contains (m-1)*n idle holes. Remaining tasks fill them column by column, and if they run out first the frame length is the answer. If instead there are more tasks than holes, every hole is filled and the extras append to the end with no idling anywhere, so the schedule is exactly len(tasks) long. Taking the maximum of the two expressions covers both regimes in one line, with no case analysis.

04

Solution & live demo

python
1from collections import Counter
2 
3class Solution:
4 def leastInterval(self, tasks: List[str], n: int) -> int:
5 freq = Counter(tasks)
6 max_freq = max(freq.values())
7 ties = sum(1 for c in freq.values() if c == max_freq)
8 frame = (max_freq - 1) * (n + 1) + ties
9 # holes in the frame absorb the rest, or overflow past it
10 return max(len(tasks), frame) #@fill
05

Edge cases

n = 0, no cooldown

The frame collapses to (m-1)*1 + ties, which never exceeds len(tasks), so the max returns len(tasks) — correct, since nothing forces a gap.

All tasks identical, e.g. AAAA with n=2

m = 4, ties = 1, frame = 3*3 + 1 = 10, which exceeds len(tasks) = 4. The answer is 10, mostly idle time.

Several tasks tied at the maximum frequency

The final row holds all of them side by side, which is why countOfTasksWithMaxFreq is added rather than 1.

Many distinct low-frequency tasks

The holes fill up and overflow, so len(tasks) dominates and the CPU never idles once.

06

Complexity

Time
O(N)
Space
O(1)
The counter holds at most 26 uppercase labels, so the space is bounded by the alphabet rather than by the input.