CodeGround
DashboardProblemsInterviewsAssessmentsCourses
CodeGround
DashboardProblemsInterviewsAssessmentsCourses
The Technical Interview Course

0 of 170 lessons complete

  • The smallest thing, cheaply
  • Keeping only k candidates
  • Ordering by something you compute
  • Always take the most urgent thing
  • Merging many ordered sources
  • Two heaps facing each other
  • The interval problem that needed a heap
  • Retrieval: when is it a heap?

Heaps and Priority Queues

84 / 170

Heaps and Priority Queues/Lesson 4 of 8·About 45 min

Always take the most urgent thing

A heap plus a waiting area, which is the shape of nearly every scheduling problem.

Key idea

Two collections, not one

Scheduling problems have items that are ready and items that are not ready yet. A heap holds what is available, ordered by urgency. A separate queue holds what is waiting, in the order it becomes available.

Each tick of the clock: move anything that has become available into the heap, then take the most urgent item from the heap. Two structures, one loop.

Why it works

Why taking the most urgent is safe

The greedy choice is to always run whatever is most pressing among the currently available options. For problems where every item takes the same time and delay is what you are minimizing, that is provably optimal by an exchange argument like the one in U6.

It is not automatically right for every scheduling problem. When items have different durations, or when future arrivals change what is best, greedy can fail. Check that the exchange argument holds before assuming it.

Here is a single tick in isolation: two tasks with three runs each, one of them chosen and parked until it is allowed to run again.

1import heapq2from collections import deque3 4available = [-3, -3]                 # remaining counts, negated for a max-heap5heapq.heapify(available)6waiting = deque()                    # (ready_time, remaining_count)7cooldown = 28time = 19 10remaining = heapq.heappop(available) + 1        # ran one, so one fewer11print("just ran a task with", -remaining, "left")12if remaining != 0:13    waiting.append((time + cooldown + 1, remaining))14 15print("available:", available)16print("waiting:", list(waiting))
Negation is what makes heapq hand back the task with the most left to do.

Tip

Idle ticks are forced, and the clock still moves

Three of each of two tasks with a gap of two takes 8 ticks: a, b, idle, a, b, idle, a, b. The idle ticks happen because nothing is available yet, and the clock advances anyway.

That is the detail worth building the loop around. Time moves whether or not work happens, so the tick counter has to be advanced before the availability check rather than only when something runs. A loop that skips ahead to the next ready time is a different and more fiddly design.

Why it works

Why waiting is a queue and not a heap

Items enter the waiting area in the order they finish and become available in that same order, because the cooldown is the same for everyone. So the front of a plain queue is always the next to become ready, and no ordering structure is needed.

If cooldowns varied per item, that would stop being true and the waiting area would need to be a heap ordered by ready time. Noticing which of your collections actually needs ordering is worth a sentence in an interview; using a heap where a queue suffices is a small but real signal.

Tip

There is a closed form, and know both

Problems of this exact shape have an arithmetic answer based on the most frequent item and how many share that frequency, computable in one pass with no simulation at all.

The formula is faster and much easier to get subtly wrong, and it does not generalize to variations. Simulate first, then mention that a closed form exists. If you offer only the formula, expect to be asked to justify it.

Predict the output· 5 min

Predict the output: a max-heap out of a min-heap

heapq only ever gives back the smallest item. The counts here are negated so that the largest one surfaces. Type the three lines it prints.

Python
1import heapq2 3remaining = {"a": 3, "b": 1, "c": 2}4 5heap = [-count for count in remaining.values()]6heapq.heapify(heap)7print(heap[0], "is on top")8 9taken = heapq.heappop(heap)10print("popped", taken, "which means a real count of", -taken)11 12heapq.heappush(heap, taken + 1)          # ran it once, so one fewer13print(sorted(-value for value in heap))
Problem· 28 min

Task Scheduler

Read the constraints first and let them tell you what complexity is expected. Derive the approach, implement it, run the tests, and submit when it passes.

PreviousOrdering by something you compute
NextMerging many ordered sources

0 of 3 parts complete