Problem overview
least_interval receives tasks: Vec<char>, uppercase letters from A to Z, and a cooldown n: i32. Equal letters are copies of the same task. Each unit of time, the CPU runs one task or idles. Two copies of the same task must have at least n units between them. The tasks can run in any order, and the function returns the fewest units of time needed to run them all.
For AAABBB with n = 2 the answer is 8: A B idle A B idle A B. With only two kinds of task, the CPU has to idle twice. With n = 0 the same tasks take 6 units, because nothing is ever forced to wait. For AAAAAABCDEFG and n = 2 the answer is 16: the six A's need gaps, the other tasks fill the first ones, and the CPU idles in the rest.
Approach: always run the task with the most copies left
Only the number of copies of each task matters, not the order of the input. The task with the most copies left is the hardest to place, because its copies need the most room between them. Starting it early leaves the fewest idle units at the end. So the plan is a simulation: in each unit, run the task with the most copies left, then keep it waiting until its cooldown is over.
Two structures do the work:
- A max-heap of counts holds the tasks that are ready to run. Its top is the biggest count.
- A queue (
VecDeque) holds the tasks that are cooling down, as pairs(left, readyTime). Tasks join the queue in the order they run, so they leave it in order of ready time: the front is always the first to be ready.
When the heap is empty but tasks are still cooling, the CPU idles. Instead of counting idle units one by one, the simulation jumps the clock straight to the moment the first waiting task is ready. There are at most 26 kinds of task, so the heap is tiny, and each task copy costs a constant number of heap operations.
Step by step
- Count the copies:
let mut count = [0; 26];, one place per letter, index 0 forAup to 25 forZ. - Loop over
tasksand add one to the count of each task's letter, at index(task as u8 - b'A') as usize.task as u8is the letter's code andb'A'the code ofA, soAmaps to 0 andZto 25. An index must be ausize. - Build the heap:
let mut heap = BinaryHeap::new();and push every countcwithc > 0. Letters that never appear have nothing to run. - Create the cooldown queue:
let mut cooling: VecDeque<(i32, i32)> = VecDeque::new();. Each entry is(left, readyTime): the copies the task has left and the time unit at whose end its cooldown is over. It can run again one unit later. - Start the clock:
let mut time = 0;. It counts the units used so far and is the answer once everything has run. - Simulate with
while !heap.is_empty() || !cooling.is_empty(). The work is done only when nothing is ready and nothing is cooling. - Start a new unit:
time += 1. - If the heap is empty, nothing can run, so
time = cooling[0].1: the idle units pass at once, up to the moment the front task is ready. - Otherwise run a task:
let left = heap.pop().unwrap() - 1;. The heap is not empty here, sounwrap()is safe. Running a task uses up one copy, soleftis what remains. - Queue it if copies remain: if
left > 0,cooling.push_back((left, time + n)). A task run attimecan run again attime + n + 1, afternother units. - Release the front task if it is ready now: if
!cooling.is_empty() && cooling[0].1 == time, take it withpop_front().unwrap()and push its count (.0) back onto the heap. - Return
time, the unit in which the last task ran.
A run for AAABBB with n = 2, printing what happens in each pass of the loop:
time 1: run a task, 2 left
time 2: run a task, 2 left
idle, jump to time 3
time 4: run a task, 1 left
time 5: run a task, 1 left
idle, jump to time 6
time 7: run a task, 0 left
time 8: run a task, 0 left
answer 8
Units 1 and 2 run the two tasks, which both have two copies left. The heap is then empty, so the clock jumps to 3, the ready time of the first task, which goes back into the heap. Unit 3 is the idle one, and units 4 and 5 run the next copies. The same pattern repeats, and the last two copies run in units 7 and 8.
The solution in Rust
use std::collections::BinaryHeap;
use std::collections::VecDeque;
impl Solution {
fn least_interval(tasks: Vec<char>, n: i32) -> i32 {
let mut count = [0; 26];
for task in tasks {
count[(task as u8 - b'A') as usize] += 1;
}
let mut heap = BinaryHeap::new();
for c in count {
if c > 0 {
heap.push(c);
}
}
let mut cooling: VecDeque<(i32, i32)> = VecDeque::new();
let mut time = 0;
while !heap.is_empty() || !cooling.is_empty() {
time += 1;
if heap.is_empty() {
time = cooling[0].1;
} else {
let left = heap.pop().unwrap() - 1;
if left > 0 {
cooling.push_back((left, time + n));
}
}
if !cooling.is_empty() && cooling[0].1 == time {
heap.push(cooling.pop_front().unwrap().0);
}
}
time
}
}
The Rust details worth a closer look:
- Letters to indexes. A
charconverts tou8withasfor ASCII letters.b'A'is a byte literal, theu8value 65, sotask as u8 - b'A'is a number from 0 to 25. The finalas usizeturns it into a valid index. - Inferred types.
countis an array of 26 numbers whose type Rust infers asi32, from how the counts are used withcooling. The heap holds those counts, so it is a max-heap ofi32. - Iterating an array.
for c in countgoes over the array's values by value, which is allowed for arrays of numbers in edition 2021. - Tuples. The entries of
coolingare(i32, i32)tuples..0is the first item and.1the second. VecDeque. It adds at the back withpush_backand removes from the front withpop_front, both in constant time. Indexing it, as incooling[0], reads the front without removing it.unwrap().pop()andpop_front()return anOption. Each call is guarded: the heap is known to be non-empty in theelsebranch, and the queue was just checked.- Last expression.
timehas no semicolon, so it is the function's value.
Complexity
Time: O(T log 26), where T is the number of tasks. Counting is O(T). Each copy of a task is popped from the heap once and pushed back at most once, and the heap never holds more than 26 counts, so each operation costs O(log 26). Idle stretches are skipped with one jump instead of one pass per unit, so a large cooldown does not slow the loop.
Space: O(1). The count array has 26 places, and the heap and the queue each hold at most 26 entries.
Edge cases and common mistakes
- No cooldown. With
n = 0,AAABBBgives 6. A task is ready again in the very next unit, so nothing idles and the answer is the number of tasks. - One task kind.
AAAAAAwithn = 2gives 16: six runs and five gaps of two idle units each, and no other task to fill them. - A single task.
Awithn = 100gives 1. The cooldown only matters between copies, so no idle time is added at the end. - Two copies and a long cooldown.
AAwithn = 3gives 5: A, three idle units, A. - Plenty of other tasks.
ABCDwithn = 5gives 4: every task has one copy, so nothing needs to wait. - Largest input. 100,000 copies of
Awithn = 100gives 10,099,900, which fits easily in ani32. - Counting idle units one at a time. It works, but it costs time proportional to the cooldown. The jump to the next ready time avoids that.
- Running the task with the fewest copies. It leaves the big counts for the end, where they must idle. The heap must give the biggest count first.
Testing
tasks = ['A','A','A','B','B','B'],n = 2gives8.- The same tasks with
n = 0give6. - Six
Aand one each ofBtoG, withn = 2, give16. ['A']withn = 100gives1.['A','A']withn = 3gives5.AAABBBCCwithn = 2gives8, the number of tasks.
