Problem overview
A CPU has a list of tasks, each an uppercase letter; equal letters are copies of the same task. Running one task takes one unit of time, and in each unit the CPU either runs a task or stays idle. Two copies of the same task must be at least n units apart: after running a task, n other units (other tasks or idle time) must pass before it can run again. The tasks can run in any order.
The method leastInterval receives tasks as a char[] and the cooldown n, and returns the fewest units of time needed to run everything.
AAABBBwithn = 2gives 8:A B idle A B idle A B. With only two kinds of task the CPU has to idle twice.AAABBBwithn = 0gives 6: no cooldown, so no idling.AAAAAABCDEFGwithn = 2gives 16:A B C A D E A F G A idle idle A idle idle A. The six A's need two units between each other, and the other tasks fill only the first gaps.
The statement asks for O(T log 26) time, where T is the number of tasks, and O(1) extra space: at most 26 counts.
Approach: always run the task with the most copies left
Only the number of copies of each task matters, not which letter it is. So the first step is to count the letters: an int[26] where index task - 'A' holds the count.
The CPU is then simulated. Which task should run in a free unit? The one with the most copies left: its copies need the most room between them, so starting them early leaves the fewest idle units at the end. A max-heap of counts gives that task in O(log 26).
After a task runs, it can't run again for a while. So it waits in a queue, together with the time at the end of which its cooldown is over. Tasks join the queue in the order they run, so they also leave it in order of their ready time, and a plain FIFO queue is enough. When the heap is empty but tasks are still cooling down, the CPU idles. Instead of counting idle units one by one, the time jumps straight to the moment the first waiting task is ready.
Here is the simulation for AAABBB with n = 2. Each line shows the time after that turn, what happened, the heap and the number of tasks cooling:
time 1: run a task, 2 left | heap [3] cooling 1
time 2: run a task, 2 left | heap [] cooling 2
time 3: jump to 3, back in heap with 2 | heap [2] cooling 1
time 4: run a task, 1 left, back in heap with 2 | heap [2] cooling 1
time 5: run a task, 1 left | heap [] cooling 2
time 6: jump to 6, back in heap with 1 | heap [1] cooling 1
time 7: run a task, 0 left, back in heap with 1 | heap [1] cooling 0
time 8: run a task, 0 left | heap [] cooling 0
The "jump" lines are the idle units, and the final time is 8. The two jumps happen at units 3 and 6.
Step by step
- Count the copies. Declare
int[] count = new int[26]; Java fills it with zeros. - Loop over the tasks with
for (char task : tasks)and docount[task - 'A']++. Letters are numbers in alphabetical order, so subtracting'A'turns A into 0, B into 1, up to Z as 25. - Build the heap: a
PriorityQueue<Integer>namedheapwith the comparator(x, y) -> y - x, so the biggest count comes out first. - Fill it: for each
cincount, addcto the heap whenc > 0. Letters that never appear have nothing to run. - Create the waiting queue:
Queue<int[]> cooling = new LinkedList<>(). Each entry is{left, readyTime}: the copies left and the time unit at whose end the cooldown is over. - Start the clock:
int time = 0. It counts the units used so far and ends as the answer. - Loop while there is work:
!heap.isEmpty() || !cooling.isEmpty(). The job is done only when nothing is ready and nothing is cooling. - Start a new unit:
time++. - If the heap is empty, nothing can run, so the CPU idles until the first waiting task is ready:
time = cooling.peek()[1]. This counts all the idle units at once. - Otherwise run a task.
int left = heap.poll() - 1takes the biggest count and uses up one copy. - Queue it if copies remain. If
left > 0, addnew int[]{left, time + n}tocooling. A task run attimecan run again attime + n + 1, afternother units. Tasks with no copies left are done. - Release a task whose cooldown is over. If
coolingis not empty andcooling.peek()[1] == time, take it out withpoll()and add its count, item[0], back toheap. - Return
time, the unit in which the last task ran.
The solution in Java
class Solution {
public int leastInterval(char[] tasks, int n) {
int[] count = new int[26];
for (char task : tasks) {
count[task - 'A']++;
}
PriorityQueue<Integer> heap = new PriorityQueue<>((x, y) -> y - x);
for (int c : count) {
if (c > 0) {
heap.add(c);
}
}
Queue<int[]> cooling = new LinkedList<>();
int time = 0;
while (!heap.isEmpty() || !cooling.isEmpty()) {
time++;
if (heap.isEmpty()) {
time = cooling.peek()[1];
} else {
int left = heap.poll() - 1;
if (left > 0) {
cooling.add(new int[]{left, time + n});
}
}
if (!cooling.isEmpty() && cooling.peek()[1] == time) {
heap.add(cooling.poll()[0]);
}
}
return time;
}
}
Java details worth a closer look:
chararithmetic.task - 'A'is anint: the difference of the two character codes, so it can index the array directly.- Max-heap through a comparator.
PriorityQueueis a min-heap by default.(x, y) -> y - xreverses it. Subtraction is safe here because counts are small. QueueandLinkedList. The variable has the interface typeQueue<int[]>, andLinkedListimplements it.addputs an entry at the back,peekreads the front without removing it, andpollremoves it.- Auto-unboxing.
heap.poll() - 1unboxes theIntegerto anint.heap.add(c)boxes anintback. The heap is never empty at the poll, because that line is in theelsebranch ofheap.isEmpty(). - Arrays as entries. A two-element
int[]is a light way to keep a count and a time together without a class.
Another way: counting
The simulation also has a closed form. If the most frequent task appears m times and c different tasks appear that often, the answer is the larger of tasks.length and (m - 1) * (n + 1) + c. The first term lays out m - 1 full blocks of n + 1 units and a last partial block holding the c most frequent tasks. The second covers the case where there are so many other tasks that no idling is needed.
static int shortcut(char[] tasks, int n) {
int[] count = new int[26];
for (char t : tasks) count[t - 'A']++;
int m = 0;
for (int c : count) m = Math.max(m, c);
int c = 0;
for (int x : count) if (x == m) c++;
return Math.max(tasks.length, (m - 1) * (n + 1) + c);
}
This gives 8 for AAABBB with n = 2, 16 for the six A's example and 6 for AAABBB with n = 0, the same as the heap solution. The lesson builds the simulation, which shows how the cooldown actually works.
Complexity
Time: O(T log 26). Counting is O(T). Each simulated turn runs one task, with one heap poll and at most one heap add, and the heap never holds more than 26 counts, so each costs O(log 26). The idle jumps skip units without extra turns, so there are at most T turns that run a task, and the number of jump turns is bounded by the same number.
Space: O(1). The count array has 26 entries, the heap and the queue each hold at most 26 entries, and nothing grows with T.
Edge cases and common mistakes
- One task.
Awithn = 100gives 1. No second copy exists, so there is nothing to wait for. - Two copies.
AAwithn = 3gives 5: A, three idle units, A. - A large cooldown.
AAABBBwithn = 50gives 104: the second round starts 51 units after the first, and the third 51 after that, so the last B runs at unit 104. - All different.
ABCDwithn = 5gives 4, since no task repeats. - Enough other tasks.
AAABBCwithn = 2gives 7, with idle time only where nothing else is ready. - Using a min-heap. Running the task with the fewest copies left first can leave the long chains for the end, where they add idle units.
- Off-by-one in the cooldown. Storing
time + n + 1or comparing withtime + 1makes tasks wait one unit too long or run one unit too soon. - Counting idle units one at a time. It works, but the jump keeps the loop short when
nis large.
Testing
AAABBB,n = 2gives8.AAABBB,n = 0gives6.AAAAAABCDEFG,n = 2gives16.A,n = 100gives1.AA,n = 3gives5.
In the lesson's code the input is a char[], so AAABBB stands for {'A', 'A', 'A', 'B', 'B', 'B'}.
