621. Task Scheduler

The most frequent task forces a minimum frame size on the schedule. Count frequencies, find the max, then a single (maxCount - 1) * (n + 1) + numMax formula gives the answer — no simulation needed.

MediumGreedyFrequency CountMath FormulaTypeScript

PROBLEM What we're solving

Given a list of tasks (uppercase letters) and a cooldown n, return the minimum number of CPU intervals needed to finish all tasks. The same task type must be at least n intervals apart; idle slots are allowed.

Example: tasks = ["A","A","A","B","B","B"], n = 2. The answer is 8. One optimal schedule: A B idle A B idle A B — A and B each appear 3 times, cooldown 2, two idle slots required.

KEY IDEA The most-frequent task dictates the frame

Insight → whichever task appears most often (call its count maxCount) creates maxCount - 1mandatory "gaps" between its executions. Each gap must be at least n wide. So the minimum time is (maxCount - 1) * (n + 1) + numMax, where numMax counts how many tasks share that max frequency. But if there are so many distinct tasks that they fill all gaps without idle time, the answer is just tasks.length.

RECIPE Count, find max, apply formula

  • 1 · Count frequencies. Iterate tasks and build a frequency map. We only care about the counts, not which letter is which.
  • 2 · Find maxCount. Scan all values; record the highest frequency. This task type is the bottleneck that defines the minimum schedule length.
  • 3 · Count numMax. How many task types share maxCount? Each of them fills the last partial frame.
  • 4 · Apply (maxCount - 1) * (n + 1) + numMax. This is the length of the schedule if idle slots are needed: maxCount - 1 full frames of width n + 1, plus the final partial frame of width numMax.
  • 5 · Take Math.max(formula, tasks.length). If tasks are so numerous that they pack all gaps, no idle time is needed and the answer equals the raw task count.
Classic confusion →many people wonder "why not simulate with a priority queue?" You can — that approach also works in O(n log k) — but the greedy formula is O(n) and requires no heap. The confusion: the formula only cares about how many tasks share the max frequency, not which tasks they are. Two tasks with count 3 behave identically regardless of their letter.

COST Complexity & alternatives

Greedy simulation (priority queue)
O(n log k)
k distinct task types; heap keeps order each slot.
Math formula (greedy insight)
O(n)
One frequency pass; one max pass; O(k) space for k ≤ 26.

Space note

Because tasks are uppercase letters, the frequency map has at most 26 entries — O(1)space in practice. The formula approach is the one to code in an interview; the simulation is worth knowing as the "how would you prove it?" follow-up.

Pattern transfer →the same "most-frequent item constrains the schedule" insight appears in Reorganize String (LC 767 — interleave so no two adjacent are the same), Rearrange String k Distance Apart(LC 358), and any "allocate slots with cooldown" variant. Whenever a cooldown or spacing constraint exists, count frequencies first.

RUN IT Count frequencies, find the max, apply the formula

step 0 / 11
STARTCount task frequencies. 6 tasks total, cooldown n = 2.
1function leastInterval(tasks: string[], n: number): number {
2 // Count task frequencies
3 const freq = new Map<string, number>();
4 for (const t of tasks) freq.set(t, (freq.get(t) ?? 0) + 1);
5
6 // Find maximum frequency and how many tasks share it
7 let maxCount = 0;
8 for (const v of freq.values()) maxCount = Math.max(maxCount, v);
9 let numMax = 0;
10 for (const v of freq.values()) if (v === maxCount) numMax++;
11
12 // Greedy formula: (maxCount - 1) gaps of size (n + 1), plus tasks that share max
13 const formula = (maxCount - 1) * (n + 1) + numMax;
14
15 // If tasks are dense enough they fill every idle slot, no idle needed
16 return Math.max(formula, tasks.length);
17}
tasksAAABBB
Letter countsbuilding frequency map
empty
current task (+1)ties for max frequencyfinal answer
slowfast

TYPESCRIPT The solution, annotated

taskScheduler.ts
function leastInterval(tasks: string[], n: number): number {
  // Count task frequencies
  const freq = new Map<string, number>();
  for (const t of tasks) freq.set(t, (freq.get(t) ?? 0) + 1);

  // Find maximum frequency and how many tasks share it
  let maxCount = 0;
  for (const v of freq.values()) maxCount = Math.max(maxCount, v);
  let numMax = 0;
  for (const v of freq.values()) if (v === maxCount) numMax++;

  // Greedy formula: (maxCount - 1) gaps of size (n + 1), plus tasks that share max
  const formula = (maxCount - 1) * (n + 1) + numMax;

  // If tasks are dense enough they fill every idle slot, no idle needed
  return Math.max(formula, tasks.length);
}

Reading it block by block

Lines 2–3 — build the frequency map. We only need counts. A Map<string, number> works; so would a number[26] fixed array. The ?? 0 handles the first occurrence.
Lines 5–6 — find the maximum frequency. One pass through freq.values(). This is the bottleneck count — the task type that runs the most often determines how many gaps we must space out.
Lines 7–8 — count numMax. Multiple task types may share the maximum count. Each one occupies a slot in the final partial frame, so they add to the formula.
Line 11 — the formula. (maxCount - 1) full frames, each of width n + 1 (one task + n cooldown slots), plus the last partial frame of numMax tasks. This equals the minimum time if idle slots are needed.
Line 14 — the floor. If there are enough varied tasks to fill every idle slot,tasks.length is the true answer. Math.max picks whichever is larger — idle slots can never make the schedule shorter than the raw count.
Complexity → O(n) time — one pass to count, two passes over at most 26 bucket values. O(k) space for k distinct task types; bounded by 26, so effectively O(1).

INTERVIEWFollow-ups they'll ask

  • "Can you return the actual schedule, not just the length?" Yes — use a max-heap simulation: greedily pop the highest-count task each slot; after executing it, decrement and re-insert after n intervals.
  • "What if tasks have weights / durations?"The formula breaks down when tasks take more than one unit; you'd need a simulation with a priority queue that tracks finish times.
  • "What if n = 0?" No cooldown — every schedule is valid, answer is just tasks.length. The formula still gives the right answer: (maxCount - 1) * 1 + numMax tasks.length.
  • "Can there be multiple optimal schedules?" Yes — many orderings achieve the minimum length. The formula gives the length; the simulation gives one valid ordering.
  • "Brute force?" Try all permutations and check cooldown constraints — O(k!) for k distinct tasks, completely infeasible. Even a simulation BFS is unnecessary given the closed-form formula.

OPTIMAL Greedy

taskScheduler.ts
function leastInterval(tasks: string[], n: number): number {
  // Count task frequencies
  const freq = new Map<string, number>();
  for (const t of tasks) freq.set(t, (freq.get(t) ?? 0) + 1);

  // Find maximum frequency and how many tasks share it
  let maxCount = 0;
  for (const v of freq.values()) maxCount = Math.max(maxCount, v);
  let numMax = 0;
  for (const v of freq.values()) if (v === maxCount) numMax++;

  // Greedy formula: (maxCount - 1) gaps of size (n + 1), plus tasks that share max
  const formula = (maxCount - 1) * (n + 1) + numMax;

  // If tasks are dense enough they fill every idle slot, no idle needed
  return Math.max(formula, tasks.length);
}
Complexity → O(n) time — one pass to count, two passes over at most 26 bucket values. O(k) space for k distinct task types; bounded by 26, so effectively O(1).

ALT 1 Brute force — simulate the schedule slot by slot

O(T) time · O(1) space

Forget the formula: actually run the clock. Each interval, pick the available task with the highest remaining count, mark it on cooldown for the next n slots, and count every tick (including forced idles) until all tasks are done.

approach-2.ts
function leastInterval(tasks: string[], n: number): number {
  const freq = new Array<number>(26).fill(0);
  for (const t of tasks) freq[t.charCodeAt(0) - 65]++;

  // cooldown[i] = the next interval at which task i is allowed to run again.
  const cooldown = new Array<number>(26).fill(0);
  let remaining = tasks.length;
  let time = 0;

  while (remaining > 0) {
    time++;
    // Among tasks with work left AND off cooldown, pick the most frequent.
    let best = -1;
    for (let i = 0; i < 26; i++) {
      if (freq[i] > 0 && cooldown[i] <= time) {
        if (best === -1 || freq[i] > freq[best]) best = i;
      }
    }
    if (best === -1) continue; // everything is cooling down -> idle this slot
    freq[best]--;
    remaining--;
    cooldown[best] = time + n + 1; // can't reuse until n slots have passed
  }

  return time;
}
Note → Correct and easy to trust, but it literally steps through every interval — including idle ones — so its cost is the total schedule length T, which the closed-form (maxCount-1)*(n+1)+numMax computes in O(n) with no loop at all. Use the simulation only when you must emit the actual ordering, not just its length.

MNEMONIC The one-liner

"The busiest task drills the frame: (maxCount-1) gaps of width (n+1), plus the last row of equals."

TRIGGERS When you see ___ → reach for ___

"cooldown / n intervals apart" + minimize total timefrequency count + (maxCount-1)*(n+1)+numMax formula
most frequent element constrains spacingfind maxCount, count ties
answer is max(formula, input size)formula floor = tasks.length when tasks are dense
"reorganize / rearrange string" variantssame frequency-bottleneck idea

SKELETON The reusable shape

skeleton.ts
function leastInterval(tasks: string[], n: number): number {
  const freq = new Map<string, number>();
  for (const t of tasks) freq.set(t, (freq.get(t) ?? 0) + 1);

  let maxCount = 0;
  for (const v of freq.values()) maxCount = Math.max(maxCount, v);
  let numMax = 0;
  for (const v of freq.values()) if (v === maxCount) numMax++;

  const formula = (maxCount - 1) * (n + 1) + numMax;
  return Math.max(formula, tasks.length);
}

FLASHCARDS Tap to flip

What does maxCount represent?
The highest task frequency — the task type that runs the most and sets the minimum schedule length.
tap to flip
1 / 8
Answer to reveal explanations. No penalty for retries.Score: 0 / 9
QUESTION 01
For tasks = ["A","A","A","B","B","B"] and n = 2, what is the minimum number of intervals?
QUESTION 02
For tasks = ["A","A","A","B","B","B","C","C","C","D","D","E"] and n = 2, the answer is:
QUESTION 03
What does numMax represent in the formula?
QUESTION 04
What is the time complexity of the greedy formula approach?
QUESTION 05
Why do we take Math.max(formula, tasks.length) rather than just returning the formula?
QUESTION 06
tasks = ["A","A","B","B"], n = 0. Answer?
QUESTION 07
In the simulation (heap) approach, what does the heap store?
QUESTION 08
#621 · Task SchedulerThe most frequent task determines idle time: (maxCount−1)×(n+1)+numWithMaxCount, floored at the total task count. No simulation needed — just count frequencies.Which algorithmic approach does this primarily use?
QUESTION 09
#621 · Task SchedulerThe most frequent task determines idle time: (maxCount−1)×(n+1)+numWithMaxCount, floored at the total task count. No simulation needed — just count frequencies.Which implementation correctly solves it?