Problem overview
The function leastInterval(tasks: CharArray, n: Int): Int receives a list of tasks, each an uppercase letter from A to Z, where equal letters are copies of the same task. Running one task takes one unit of time, and in every unit the CPU either runs one task or idles. After a task runs, the CPU needs n other units (other tasks or idle ones) before it can run that same task again. The tasks can run in any order, and the function returns the fewest units needed to run all of them.
For tasks = A A A B B B and n = 2 the answer is 8. One best order is A B idle A B idle A B. Two kinds of task can't fill the gaps between copies, so the CPU idles twice. With n = 0 there is no cooldown, any order works, and the answer is 6. For six A's, one each of B to G, and n = 2, the answer is 16: the six A's force a gap pattern, the other tasks fill the first gaps, and the CPU idles in the rest.
The lesson 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
The names of the tasks don't matter, only how many copies each has. The idea is to simulate the CPU one unit at a time with a greedy rule: run the task with the most copies left. Tasks with many copies need the most room between their runs, so starting them early leaves the fewest idle units at the end.
Two structures support the simulation:
- A max-heap of counts holds the tasks that are ready to run. The biggest count is on top. The heap never holds more than 26 entries, one per letter, so each operation costs
O(log 26). - A queue named
coolingholds tasks that just ran and are waiting out their cooldown. Each entry isintArrayOf(left, readyTime): the copies left and the time unit at whose end the cooldown is over. Tasks enter the queue in the order they run, so they also become ready in that order, and the front is always the next one to return.
When the heap is empty but tasks are still cooling, the CPU can only idle. Rather than count idle units one by one, the simulation jumps the clock to the moment the first waiting task is ready. This keeps the work proportional to the number of tasks, not to the idle time.
A short counting formula also exists: if the most frequent task appears m times and c tasks appear that often, the answer is the bigger of tasks.size and (m - 1) * (n + 1) + c. This lesson builds the simulation, which also shows why the formula holds.
Step by step
- Count the copies. Create
countasIntArray(26), one place per letter, filled with zeros. - Loop over
tasks, naming each onetask. - Add one to the letter's count at index
task - 'A'. Subtracting'A'turns A into 0, B into 1, up to Z as 25. - Create the heap, a
PriorityQueue<Int>withcompareByDescending { it }, so the biggest count comes out first. - Loop over
count, naming each countc. - Add only the non-zero counts to the heap. Letters that never appear have nothing to run.
- Create
coolingas an emptyArrayDeque<IntArray>(). - Start
timeat 0. It counts the units used so far, and it is the answer at the end. - Loop while
heap.isNotEmpty() || cooling.isNotEmpty(). The work is done only when nothing is ready and nothing is cooling. - Start a new unit with
time++. - If the heap is empty, no task can run, so set
timeto the ready time of the front ofcooling,cooling.first()[1]. This counts all the idle units in one step. - Otherwise run a task:
heap.poll() - 1is the number of copies it has left after this run. - If copies remain, add
intArrayOf(left, time + n)at the back ofcooling. A task that ran attimecan run again attime + n + 1, afternother units. A task with no copies left is done and isn't queued. - Release a ready task. If the front of
coolinghas a ready time equal totime, remove it and put its remaining count back into the heap. - Return
time, the unit in which the last task ran.
The solution in Kotlin
class Solution {
fun leastInterval(tasks: CharArray, n: Int): Int {
val count = IntArray(26)
for (task in tasks) {
count[task - 'A']++
}
val heap = PriorityQueue<Int>(compareByDescending { it })
for (c in count) {
if (c > 0) {
heap.add(c)
}
}
val cooling = ArrayDeque<IntArray>()
var time = 0
while (heap.isNotEmpty() || cooling.isNotEmpty()) {
time++
if (heap.isEmpty()) {
time = cooling.first()[1]
} else {
val left = heap.poll() - 1
if (left > 0) {
cooling.addLast(intArrayOf(left, time + n))
}
}
if (cooling.isNotEmpty() && cooling.first()[1] == time) {
heap.add(cooling.removeFirst()[0])
}
}
return time
}
}
Kotlin details worth a look:
- Character arithmetic. Subtracting one
Charfrom another gives anInt, sotask - 'A'is a valid array index:'C' - 'A'is 2. Adding anIntto aChargives aChar, but subtracting twoChars gives the distance between them. count[...]++. Kotlin allows++on an array element, which reads it, adds one and writes it back.- Java's heap, Kotlin's queue.
PriorityQueueisjava.util.PriorityQueue, a min-heap by default, andcompareByDescending { it }reverses it for counts.ArrayDequeis Kotlin's own, inkotlin.collections, so it needs no import. In a file of your own, addimport java.util.PriorityQueue. - Deque functions.
addLastadds at the back, andfirst()andremoveFirst()read and remove the front. Calling them on an empty deque throwsNoSuchElementException, which is why each use is guarded by an emptiness check or by the loop condition. pollreturns a nullable type.heap.poll()comes from Java and has the platform typeInt!, soheap.poll() - 1compiles. It is safe because theelsebranch only runs when the heap is not empty.- Short-circuit
&&. Incooling.isNotEmpty() && cooling.first()[1] == time, the right side runs only when the deque has an element. timeis avar. It is incremented and also assigned, when the clock jumps.
Complexity
Time: O(T log 26). Counting is O(T). Each loop turn runs one task, and every task copy runs exactly once, so there are T turns. Each turn does a heap removal and at most one heap insertion, both O(log 26) because the heap holds at most 26 counts. Idle stretches are skipped by one assignment, not by looping.
Space: O(1). The count array has 26 places. The heap and the queue together hold at most 26 letters, because a letter is in exactly one of them or finished.
Edge cases and common mistakes
- No cooldown. With
n = 0a task is ready again immediately, so the answer istasks.size.A A A B B Bgives 6. - One kind of task.
A Awithn = 100gives 102: the first A runs at unit 1, the CPU idles for 100 units, and the second A runs at unit 102. The clock jumps over the idle stretch in one step. - A cooldown that's never needed.
A B Cwithn = 5gives 3. No letter repeats, so nothing waits. - A single task.
Awithn = 100gives 1: after the last copy there is no reason to wait. - Queuing finished tasks. A task with no copies left must not go into
cooling, or the loop would wait for something that doesn't exist. - Off-by-one on the cooldown. A task that ran at
timeneedsnother units, so it returns attime + n + 1. The code storestime + n, the unit at whose end the cooldown is over, and the next turn incrementstimebefore running. - Running the smallest count first. A min-heap would leave the biggest group for last and add idle units that a better order avoids.
Testing
Trace for A A A B B B with n = 2:
time=1: run a task with 3 copies, 2 left
time=2: run a task with 3 copies, 2 left
time=3: idle until time 3, ready again with 2 left
time=4: run a task with 2 copies, 1 left, ready again with 2 left
time=5: run a task with 2 copies, 1 left
time=6: idle until time 6, ready again with 1 left
time=7: run a task with 1 copies, 0 left, ready again with 1 left
time=8: run a task with 1 copies, 0 left
Units 1 and 2 run A and B, and unit 3 is idle: both tasks are still cooling, so the heap is empty. Units 4 and 5 run them again, unit 6 is idle, and units 7 and 8 run the last copies. The total is 8.
Trace for A A with n = 100:
time=1: run a task with 2 copies, 1 left
time=101: idle until time 101, ready again with 1 left
time=102: run a task with 1 copies, 0 left
Here the clock goes from 1 to 101 in one step, then the last copy runs at unit 102.
A A A B B B,n = 2gives8.A A A B B B,n = 0gives6.- Six A's and one each of B, C, D, E, F, G, with
n = 2gives16. A,n = 100gives1.A B C,n = 5gives3.
