bigonotes
DSA: Patterns & PythonHeap / Priority Queue

Scheduling & Greedy with Regret

Use a heap to always act on the best option now, a queue to park items until they're ready, and a heap to undo your worst past choice.

7 min readLast updated

Many greedy problems need "the best option right now" again and again while the options change. A heap gives you that in O(log⁡n)O(\log n). This note covers two shapes: scheduling (a heap picks the top task, a queue parks tasks that aren't ready yet) and greedy with regret (commit to a choice now, and use a heap to take back the worst one later).

The idea#

Scheduling problems ask you to pick the best option at each step while some items are not ready yet. Two heap shapes cover most of them.

Heap + waiting queue#

For scheduling and ranking, use a queue for items that can only be processed after a condition, and a heap to process the top item first. For tasks with a cooldown:

  1. Pop the most frequent task from a max-heap and run it.
  2. If it still has copies left, park it in a FIFO queue with the time it becomes ready again.
  3. Before each step, move the front of the queue back into the heap if its ready time has come.

Running the most frequent task first is the greedy choice: it is the task most likely to be left over with nothing to separate its copies. The queue is FIFO because every parked task waits the same cooldown, so the earliest parked is always the earliest ready.

Greedy with regret#

Sometimes you can't tell which choice is best until later. Picture two resources, like the ladders and bricks in Furthest Building You Can Reach: one covers a need of any size per use (so it is worth most on the biggest needs), and one is paid per unit. While walking forward you can't know whether a bigger need is ahead or already behind you.

The fix is to commit now and correct later:

  1. Give every need the "any size" resource and record the need in a min-heap.
  2. When you have used more of it than you own, take it back from the smallest recorded need and pay for that need per unit instead.
  3. When the per-unit budget goes negative, you can't go further.

Why it works: the heap always holds the largest needs seen so far, so the per-unit budget is always spent on the smallest ones. That is exactly the optimal assignment, found in one pass.

How to spot it#

These cues point to a heap-driven greedy.

  • Tasks with cooldowns, or "no two equal items adjacent".
  • Events sorted by time, where you repeatedly pick the shortest, cheapest or most profitable of those currently available (sort by arrival, heap by priority). The heap might hold processing times, event end days (Maximum Number of Events), or room end times.
  • Two kinds of resource where one is "unlimited per use" (ladders, refuels) and should go to the largest needs.
  • A DP over (index, resource A, resource B) that is far too big. That often hints at a regret heap.

Template#

Here are both shapes from "The idea" as code. The cooldown shape uses one heap and one queue. A step where the heap is empty but the queue is not means either "no valid order" or "idle", depending on the problem.

heap = max-heap of (remaining count, task)
waiting = empty FIFO queue of (count, task, ready_at)
for each step:
    if front of waiting is ready at this step:
        move it from waiting back onto heap
    if heap is empty:
        fail, or count an idle step         # all left are cooling down
    pop the top task; run it
    if it has copies left:
        append (count - 1, task, step + cooldown + 1) to waiting

The regret shape needs only a min-heap of the needs that currently hold the "any size" resource.

held = empty min-heap                    # smallest held need on top
for each need, moving forward:
    if need <= 0: continue
    push need onto held                  # give it the resource for now
    if len(held) > resource count:
        budget -= pop smallest from held # regret: pay per unit instead
        if budget < 0: stop here

Both run in O(nlog⁡k)O(n \log k), where kk is the heap size (the alphabet, or the resource count).

Pitfalls#

Watch for these when you adapt the templates.

  • Empty heap, non-empty queue. Decide what it means for your problem: no valid order, or an idle step.
  • Wrong ready time. A task run at step t with cooldown n is ready again at t + n + 1, not t + n.
  • Reaching for DP first. A memo over (index, resource A, resource B) is often far too large; a regret heap is O(nlog⁡k)O(n \log k).
  • Using a closed-form formula without its picture. Formulas for cooldown problems hide edge terms that are easy to get wrong. Derive them from a picture of the schedule.

Problems in this pattern#

These problems use this pattern. Solve them before you open the hints below. Start with the first three.

Hints#

Open a hint only after you have tried the problem for a while.

Hint: Task Scheduler

The cooldown template works; an empty heap with a non-empty queue is an idle step. There is also a closed form. Lay the most frequent task out as max_freq - 1 frames of length n + 1, plus a last frame holding the count_max tasks with the top count.

Hint: Task Scheduler: the formula

The frames give (max_freq - 1) * (n + 1) + count_max. If there are more tasks than idle slots, no idling is needed and the answer is len(tasks). So take the max of the two.

Hint: Reorganize String

This is the cooldown template with cooldown = 1. If the heap is empty while the queue still holds a character, no valid order exists: return "".

Hint: Furthest Building You Can Reach

A ladder covers a climb of any height, so ladders belong on the biggest climbs. Give every climb a ladder and push it into a min-heap. When the heap holds more climbs than you have ladders, pop the smallest and pay for it with bricks. A negative brick count means you stop at this building.

Hint: Minimum Number of Refueling Stops

Regret in the other direction. Drive as far as you can, pushing every station you pass into a max-heap of fuel. When you run dry, refuel from the best station you already passed, as if you had stopped there.

Hint: Longest Happy String

Use a max-heap by remaining count, as in the cooldown problems. If the top letter would make three in a row, use the second letter once instead, then push both back.

Hint: Single-Threaded CPU

Sort tasks by arrival and push each into a min-heap by processing time as soon as it is available. When the heap is empty, jump the clock to the next arrival.

Hint: Maximum Performance of a Team

Sort engineers by efficiency, highest first, so the current engineer sets the minimum. Keep a min-heap of the chosen speeds; when it holds more than k, drop the slowest.

Hint: Meeting Rooms III

Sort meetings by start. Keep two min-heaps: free rooms by number, and busy rooms by end time. Free every room whose meeting has ended before you place the next one. If no room is free, delay the meeting until the earliest busy room ends.

Hint: Minimum Interval to Include Each Query

Sort intervals by start and queries by value, keeping each query's original index. For each query, push every interval that has started into a min-heap keyed by size and pop the ones that end before the query. The heap top is that query's answer.

Key takeaways#

  • A heap picks the best task now; a FIFO queue holds tasks until their cooldown ends.
  • Most frequent first is the greedy choice for cooldown problems.
  • Greedy with regret: commit to a choice now and keep it in a heap, so you can undo the worst one later.
  • A ladder is worth any number of bricks, so ladders belong on the largest climbs.
  • Sort by availability, then heap by priority, for jobs that arrive over time.
Revision notes · 2 min

Core idea#

A heap gives "the best option right now" in O(log⁡n)O(\log n) while the options change. In scheduling, a heap picks the top task and a queue parks tasks that are not ready. In greedy with regret, you commit now and use a heap to take back the worst choice later.

Key points#

  • Cooldown (Task Scheduler, Reorganize String): pop the most frequent task from a max-heap and run it.
  • Park a task with copies left in a FIFO queue with its ready time. Move it back to the heap when that time comes.
  • The queue is FIFO because every task waits the same cooldown.
  • Most frequent first: that task is the most likely to be left over with nothing to separate its copies.
  • Furthest Building: give every climb a ladder and push the climb into a min-heap.
  • With more ladders used than owned, pop the smallest climb and pay bricks for it. A negative brick count means stop.
  • Refueling Stops: push passed stations into a max-heap of fuel. When dry, refuel from the best one already passed.
  • Jobs over time: sort by arrival, then heap by priority (Single-Threaded CPU, Meeting Rooms III).

Task Scheduler formula#

  • Answer: max((max_freq - 1) * (n + 1) + count_max, len(tasks)).
  • The most frequent task makes max_freq - 1 frames of length n + 1.
  • The last frame holds only the tasks with the top count.

Template#

This is the regret shape.

held = empty min-heap                    # smallest held need on top
for each need, moving forward:
    if need <= 0: continue
    push need onto held                  # give it the resource for now
    if len(held) > resource count:
        budget -= pop smallest from held # regret: pay per unit instead
        if budget < 0: stop here

Pitfalls#

  • Empty heap, non-empty queue. Reorganize String: no valid order. Task Scheduler: an idle step.
  • Wrong ready time. A task run at step t with cooldown n is ready at t + n + 1.
  • DP over (index, bricks, ladders). It is far too large. The regret heap is O(nlog⁡k)O(n \log k) for kk ladders.
  • The formula without the frame picture. The count_max term and the max guard are easy to get wrong.

Say it in an interview#

  • "Ladders cover any height, so they belong on the largest climbs. The heap lets you move one back later."
  • "A heap picks the most frequent task, and a queue holds tasks during their cooldown."

Found a mistake? Email ask@bigonotes.com