Problem overview
A CPU runs a list of tasks, given as a vector<char> named tasks. Each task is an uppercase letter, and equal letters are copies of the same task. Running one task takes one unit of time, and in each unit the CPU runs one 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) must pass before that task runs again. The tasks can run in any order.
leastInterval receives tasks and the cooldown n and returns the fewest units of time needed to run everything.
With three copies each of A and B and n = 2, the answer is 8: one best order is A B idle A B idle A B. With n = 0 the same tasks need only 6 units, in any order. With six copies of A, one each of B to G and n = 2, the answer is 16: A B C A D E A F G A idle idle A idle idle A.
The statement asks for O(T log 26) time, where T is the number of tasks, since a heap here never holds more than 26 kinds of task, and O(1) extra space, since there are at most 26 counts.
Approach: always run the task with the most copies left
Only the number of copies of each task matters, not the order in the input, so the first step is to count the letters.
Then simulate the CPU, one unit at a time, with one rule: run the task that has 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 has to wait. Running it at time t lets it run again at time t + n + 1, after n other units. So it goes into a queue of cooling tasks as the pair {left, readyTime}: the copies it has left, and the unit at whose end its cooldown is over, which is t + n. Tasks enter the queue in the order they run, so they leave it in order of ready time, and only the front can be ready.
When the heap is empty but tasks are still cooling, the CPU has nothing to run. Instead of counting idle units one by one, the time jumps straight to the ready time of the first waiting task, which counts all the idle units at once.
Here is the simulation for three As and three Bs with n = 2, as a trace of the loop:
time=1: run task with 3 copies, 2 left
time=2: run task with 3 copies, 2 left
time=3: idle until here
ready again: 2 copies
time=4: run task with 2 copies, 1 left
ready again: 2 copies
time=5: run task with 2 copies, 1 left
time=6: idle until here
ready again: 1 copies
time=7: run task with 1 copies, 0 left
ready again: 1 copies
time=8: run task with 1 copies, 0 left
The first task to run (the A, at time 1) becomes ready again at the end of unit 3. Units 1 and 2 run the two tasks, unit 3 is idle, and the cooled-down task is back in the heap for unit 4. The last task runs at unit 8, so the answer is 8.
For six As and six other single tasks, the trace ends with jumps. At time 10 the A runs its fifth copy, and its next copy is ready at the end of unit 12, so units 11 and 12 are idle and the loop moves from 10 straight to 12. The same happens between units 13 and 16, and the answer is 16.
Step by step
- Create
count, avector<int>of 26 zeros, one per letter: index 0 for A up to 25 for Z. - Loop over
taskswith achar task. - Count each task with
count[task - 'A']++. Letters are stored as consecutive numbers, so subtracting'A'turns A into 0 and Z into 25. - Declare the heap:
priority_queue<int> heap, a max-heap, so the biggest count is on top. - Loop over
countwith anint c. - Push the non-zero counts: a letter that never appears has nothing to run.
- Declare
coolingasqueue<pair<int, int>>: the waiting tasks as{left, readyTime}, read with.firstand.second. - Start
timeat 0. It counts the units used so far, and is the answer when everything has run. - Loop while
!heap.empty() || !cooling.empty(). The work is done only when nothing is ready and nothing is cooling. - Add one to
time: a new unit starts. - If the heap is empty, the CPU idles until the first cooling task is ready: set
timetocooling.front().second. - Otherwise run a task:
leftisheap.top() - 1, the copies remaining after this run, andheap.pop()removes the entry. - If copies remain, push
{left, time + n}ontocooling. A task with no copies left is finished and isn't queued. - Release a ready task: if
coolingisn't empty and its front has ready time equal totime, move its count back intoheapand pop the queue. The emptiness check comes first, so the front is never read from an empty queue. - Return
time, the unit in which the last task ran.
The solution in C++
class Solution {
public:
int leastInterval(vector<char>& tasks, int n) {
vector<int> count(26, 0);
for (char task : tasks) {
count[task - 'A']++;
}
priority_queue<int> heap;
for (int c : count) {
if (c > 0) {
heap.push(c);
}
}
queue<pair<int, int>> cooling;
int time = 0;
while (!heap.empty() || !cooling.empty()) {
time++;
if (heap.empty()) {
time = cooling.front().second;
} else {
int left = heap.top() - 1;
heap.pop();
if (left > 0) {
cooling.push({left, time + n});
}
}
if (!cooling.empty() && cooling.front().second == time) {
heap.push(cooling.front().first);
cooling.pop();
}
}
return time;
}
};
The C++ details worth a closer look:
- Letters are numbers. A
charis a small integer, sotask - 'A'is anintbetween 0 and 25 for uppercase letters. That makes a 26-element vector a simple counting table. priority_queue<int>is a max-heap. Itstop()is the largest value, andpop()returns nothing, so the code readstop()first and removes it second.pair<int, int>holds two values read as.firstand.second. The braces incooling.push({left, time + n})build the pair inside the call.queueis first in, first out.front()is the oldest entry. Readingfront()of an empty queue is undefined behaviour, which is why the secondiftests!cooling.empty()first, and why the idle branch is safe: the loop only runs while one of the two containers has something, and an empty heap means the queue has an entry.- The variable named
timehides the C library function of the same name inside this function. That is allowed, and the function never needs the library one. time = cooling.front().secondnever moves time backwards: tasks are queued one per unit, so their ready times are all different and increasing, and each one is released in the very unit its ready time is reached. A task still waiting therefore always has a ready time later than the unit before the jump.
Complexity
Time: O(T log 26). Counting is O(T). Every unit of the loop either runs a task, which takes one heap pop and at most one push, or jumps over idle time. Idle jumps are followed at once by a task being released, so the loop makes at most one turn per task run plus one per idle stretch, which is at most T turns of each kind. The heap holds at most 26 counts, so each heap operation is O(log 26).
Space: O(1). count has 26 entries, the heap at most 26, and the queue at most 26 pairs.
Edge cases and common mistakes
- No cooldown. With
n = 0a task is queued with ready time equal to the current unit and is released at once, so no unit is idle. ThreeAs and threeBs give 6. - A single task.
{'A'}withn = 5gives 1: there is no second copy to wait for.{'A', 'A'}withn = 3gives 5: A, three idle units, A. - Cooldown longer than the number of tasks.
{'A', 'B', 'C'}withn = 50gives 3. Each task is different, so none ever waits for itself. - Only the cooldown idles. The answer can never be smaller than
tasks.size(). When enough different tasks exist to fill every gap, the answer equals it exactly: threeAs, threeBs, threeCs, twoDs and oneEwithn = 2give 12, with no idle unit. - Running the smallest count first. If the CPU picks a task with few copies left, the most frequent task is left for the end and is forced to idle. Always picking the biggest count avoids that.
- Cooldown off by one. A task run at unit
tcan run again att + n + 1. Storingtime + nas the unit at whose end the cooldown is over, and releasing it whentimereaches that value, gives exactlynunits in between. - Counting idle units one by one. The jump keeps the loop short even for
n = 100.
Testing
{'A', 'A', 'A', 'B', 'B', 'B'}withn = 2gives 8.{'A', 'A', 'A', 'B', 'B', 'B'}withn = 0gives 6.- Six
As and one each ofB,C,D,E,F,Gwithn = 2gives 16. {'A'}withn = 5gives 1.{'A', 'A'}withn = 3gives 5.{'A', 'B', 'C'}withn = 50gives 3.- Three
As, threeBs, threeCs, twoDs and oneEwithn = 2gives 12.
