Task scheduler with cooldown

Greedily run the most frequent remaining task each tick, using a max-heap of task counts and a queue for tasks that are cooling down.

Time O(n log m)Space O(m)Code C#
Watch it animatedStep through it, with the running line highlighted. Free with an account.

The idea

Greedily run whichever remaining task has the highest count, then let it cool down for n ticks before it can run again. A max-heap ordered by remaining count always surfaces the right task to run; a cooldown queue holds tasks that are temporarily unavailable until they rejoin the heap. If nothing is available on a tick, the CPU idles.

Code

public static int LeastInterval(char[] tasks, int n) {
    var freq = new Dictionary<char, int>();
    foreach (char t in tasks) freq[t] = freq.GetValueOrDefault(t) + 1;
    var heap = new PriorityQueue<char, int>(Comparer<int>.Create((a, b) => b - a));
    foreach (var kv in freq) heap.Enqueue(kv.Key, kv.Value);
    var cooldown = new Queue<(char task, int count, int readyAt)>();
    int time = 0;
    while (heap.Count > 0 || cooldown.Count > 0) {
        time++;
        while (cooldown.Count > 0 && cooldown.Peek().readyAt <= time) {
            var ready = cooldown.Dequeue();
            heap.Enqueue(ready.task, ready.count);
        }
        if (heap.TryDequeue(out char t, out int cnt)) {
            if (--cnt > 0) cooldown.Enqueue((t, cnt, time + n + 1));
        }
    }
    return time;
}

Tips

In the .NET library

This greedy-plus-cooldown shape is the same one behind rate limiting: serve the highest-priority ready request, and anything over its limit waits in a queue until its window reopens.