Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Priority Queue / Heap

Builds on Top-K Heap, which you haven’t finished yet.

P3MediumBuilt on Top-K Heap~5 min speedrun

Event Simulation with a Ready Heap (Single-Threaded CPU, Task Scheduling)

Built on
Top-K HeapA clock decides which items have arrived, and only arrived items are in the heap.
The problem

Single-Threaded CPU (LC 1834)(opens leetcode.com)

You get n tasks numbered 0 to n - 1 as a list tasks, where tasks[i] = [enqueueTime_i, processingTime_i]: task i can start from time enqueueTime_i on, and it needs processingTime_i units of time.

One CPU runs the tasks, one at a time, and each task runs to the end without a break. Whenever the CPU is free, it looks at the tasks that have already arrived and have not run yet, and picks the one with the shortest processing time; between equal times, the smaller index wins. If no task is waiting, the CPU stays idle until one arrives. A new task can start at the very moment the previous one ends.

Return the task indices in the order the CPU runs them.

Example 1
Input:tasks = [[1,2],[2,4],[3,2],[4,1]]
Output:[0,2,3,1]
At time 1 only task 0 has arrived, so it runs from 1 to 3. By then tasks 1 and 2 are waiting and task 2 is shorter, so it runs from 3 to 5. Task 3 arrived at 4 and needs 1 unit, so it beats task 1 and runs from 5 to 6; task 1 runs last, from 6 to 10.
task 0 (arrives 1)task 2 (arrives 3)task 3 (arrives 4)task 1 (arrives 2)110

Ready Heap

items with arrival times ➔ a heap of the arrived items, ordered by the rule that picks the next one ➔ pop the best; if the heap is empty, set the clock to the next arrival
Spot it when: Items become available over time (an arrival time, a start day, a capital you must reach first), and each time the resource is free you must serve the best item that is already available: the shortest task, the lightest free server, the lowest-numbered free room, the most profitable project you can afford.
Ready Heap
Python
1import heapq
2 
3def get_order(tasks: list[list[int]]) -> list[int]:
4 """Single-Threaded CPU: run the shortest arrived task next, jumping the clock when idle.
5 Time: O(N log N) | Space: O(N)
6 """
7 order = sorted(range(len(tasks)), key=lambda i: tasks[i][0]) # Task indices by arrival time
8 ready: list[tuple[int, int]] = [] # Min-heap of (duration, index) of arrived tasks; the root runs next
9 result: list[int] = []
10 time = 0
11 ptr = 0 # Next task in arrival order not yet released
12 
13 # 1. Jump the Clock When the CPU Is Idle:
14 # ready holds nothing, so the next task has not arrived yet; jumping straight to its arrival avoids ticking time forward one unit at a time.
15 while len(result) < len(tasks):
16 if not ready and time < tasks[order[ptr]][0]:
17 time = tasks[order[ptr]][0] # CPU idle: jump the clock to the next arrival
18 
19 # 2. Release Every Task That Has Arrived:
20 # Several tasks can arrive at or before the same time, so release all of them before picking, not just the next one.
21 while ptr < len(order) and tasks[order[ptr]][0] <= time:
22 i = order[ptr]
23 heapq.heappush(ready, (tasks[i][1], i)) # Release every task that has arrived by now
24 ptr += 1
25 
26 # 3. Run the Shortest Ready Task:
27 # ready holds exactly the tasks that have arrived and not run, so its root is the CPU's next pick by the shortest-duration, smaller-index rule.
28 duration, i = heapq.heappop(ready) # Shortest arrived task, smaller index on ties
29 time += duration # It runs to the end without stopping
30 result.append(i)
31 
32 return result
Invariant: Before each pop, ready holds exactly the tasks that have arrived by time and have not run yet, so ready[0] is the task the CPU starts next.

Only the tasks that have already arrived compete for the CPU, so sort the task indices by arrival once (order) and walk them with a pointer ptr: before each choice, push every task whose arrival is at most time onto the min-heap ready as (duration, i), then pop the root, run it, and add its duration to time. time never goes back, so ptr only moves forward and each task is pushed and popped exactly once; ready always holds exactly the arrived tasks that have not run, and the tuple order puts the shortest one on top, with the smaller index winning ties, which is the CPU's rule. The trap is the idle CPU: when ready is empty the next task has not arrived yet, so jump time to its arrival, tasks[order[ptr]][0], before releasing. Without the jump heapq.heappop(ready) pops an empty heap, and moving time forward one unit at a time takes up to 10^9 steps.

See it run

Ready Heap, step by step

time 0 · ptr 0

time 1 · ptr 0

time 1 · ptr 1

time 3 · ptr 1

time 3 · ptr 3

time 5 · ptr 3

time 5 · ptr 4

time 6 · ptr 4

time 10 · ptr 4

time 12 · ptr 4

time 12 · ptr 6

time 15 · ptr 6

time 18 · ptr 6

time 20 · ptr 6

time 20 · ptr 7

time 22 · ptr 7

time 22 · ptr 7 · answer [2, 6, 0, 4, 1, 5, 3]

The columns of the first two rows are positions in arrival order: the task index, then its arrival time. The ready row is the heap array of (duration, index) tuples, shown as duration,index. The last row is result.

  • ptr the next task in arrival order that is not released yet
  • jump the arrival time that time is set to while the CPU is idle
  • pushed a task that has arrived and is now in ready
  • ran the index of the task that just ran

Start: time = 0, ptr = 0 and ready is empty. order lists the task indices by arrival, and the arrival row shows each arrival time.

ready is empty and the next task arrives at 1, after time = 0. Set time to 1 at once, not one unit at a time.

With time = 1, push (2, 2) onto ready, because that task has arrived.

Pop (2, 2). Task 2 runs for 2, so time becomes 3.

With time = 3, push (4, 4) and (2, 6) onto ready, because those tasks have arrived.

Pop (2, 6). Task 6 runs for 2, so time becomes 5.

With time = 5, push (1, 0) onto ready, because that task has arrived.

Pop (1, 0). Task 0 runs for 1, so time becomes 6.

Pop (4, 4). Task 4 runs for 4, so time becomes 10.

ready is empty and the next task arrives at 12, after time = 10. Set time to 12 at once, not one unit at a time.

With time = 12, push (3, 1) and (3, 5) onto ready, because those tasks have arrived.

Pop (3, 1). Task 1 runs for 3, so time becomes 15. Task 5 has the same duration, but task 1 has the smaller index.

Pop (3, 5). Task 5 runs for 3, so time becomes 18.

ready is empty and the next task arrives at 20, after time = 18. Set time to 20 at once, not one unit at a time.

With time = 20, push (2, 3) onto ready, because that task has arrived.

Pop (2, 3). Task 3 runs for 2, so time becomes 22.

Done: all 7 tasks ran, so return [2, 6, 0, 4, 1, 5, 3].

order
02ptr
14
26
30
41
55
63
arrival
1
2
3
4
12
12
20
ready
result
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Not jumping time forward when the CPU is idle

    Mistake
    while len(result) < len(tasks):
    Fix
    while len(result) < len(tasks):
        if not ready and time < tasks[order[ptr]][0]:
            time = tasks[order[ptr]][0]

    What happens: When ready is empty, the next task has not arrived yet. Without the jump, the release loop releases nothing. heapq.heappop(ready) then pops an empty list and raises an IndexError. Moving time forward by 1 per step would take up to 10^9 steps, because an arrival can be 10^9. Jump time straight to tasks[order[ptr]][0].

    Example: Single-Threaded CPU (LC 1834): tasks = [[1, 2], [2, 4], [3, 2], [4, 1]]
    Mistake: raises IndexErrorFix: returns [0, 2, 3, 1]
  2. 2

    Gotcha 2: Releasing with < time instead of <= time

    Mistake
    while ptr < len(order) and tasks[order[ptr]][0] < time:
    Fix
    while ptr < len(order) and tasks[order[ptr]][0] <= time:

    What happens: A task that arrives exactly at time is already available. With <, it stays out of ready. After an idle jump, time equals the next arrival, so nothing is released. heapq.heappop(ready) then raises an IndexError.

    Example: tasks = [[1, 2], [2, 4], [3, 2], [4, 1]]
    Mistake: raises IndexErrorFix: returns [0, 2, 3, 1]
  3. 3

    Gotcha 3: Releasing with if instead of while

    Mistake
    if ptr < len(order) and tasks[order[ptr]][0] <= time:
    Fix
    while ptr < len(order) and tasks[order[ptr]][0] <= time:

    What happens: Several tasks can arrive at the same time. With if, only one of them is released before each pick. The CPU then chooses among too few tasks. It can run a long task while a shorter one has already arrived.

    Example: tasks = [[7, 10], [7, 12], [7, 5], [7, 4], [7, 2]]
    Mistake: returns [0, 1, 2, 3, 4]Fix: returns [4, 3, 2, 0, 1]
  4. 4

    Gotcha 4: Pushing ptr instead of the task's index i

    Mistake
    heapq.heappush(ready, (tasks[i][1], ptr))
    Fix
    heapq.heappush(ready, (tasks[i][1], i))

    What happens: ptr is the task's position in order, sorted by arrival. i is its index in tasks. The answer must list indices in tasks, and ties go to the smaller index in tasks. With ptr, a task that arrives first gets 0, whatever its real index.

    Example: tasks = [[2, 1], [1, 3]]
    Mistake: returns [0, 1]Fix: returns [1, 0]
Not this one if…
  • Every item is there from the start and only the best k matter, as in K Closest Points to Origin (LC 973): there is no clock, and nothing becomes available later. See Top-K Heap
  • You only need how many intervals overlap at the busiest moment, as in Meeting Rooms II (LC 253), not which item is served next or where it goes. See Sweep Line
  • An item you already took can be dropped again when a later deadline is missed, as in Course Schedule III (LC 630); here a task that has started always runs to the end. See Greedy with Regret

Deep Dive

Why only the arrived tasks are in the heap

The CPU may start a task only if the task has arrived. So at any time, the legal choices are exactly the tasks with arrival at most time that have not run. The rule of Single-Threaded CPU (LC 1834) picks the shortest duration among them, and the smaller index on a tie.

ready is a min-heap of (duration, i) tuples. Tuple order is the rule itself: it compares the duration first and the index second. So ready[0] is the task the CPU starts next, with no extra comparison code.

Tasks that have not arrived stay out of ready. They are in order, sorted by arrival, and ptr marks the first one that is not released. Before each pop, the loop moves every task with arrival at most time from order into ready. This keeps the invariant: ready holds exactly the arrived tasks that have not run.

Example: for tasks = [[1, 2], [2, 4], [3, 2], [4, 1]], the CPU starts task 0 at time 1 and finishes at 3. Tasks 1 and 2 have arrived by then, and task 2 is shorter, so it runs next and ends at 5. Task 3 has arrived at 4, so now it competes with task 1, and the shorter task 3 runs. The last is task 1. The result is [0, 2, 3, 1], which is the LeetCode answer. Task 3 is the shortest task overall, but it cannot run before it arrives.

One trace: a jump, two arrivals at once, and a tie

The board replays get_order on tasks = [[4, 1], [12, 3], [1, 2], [20, 2], [2, 4], [12, 3], [3, 2]]. Sorted by arrival, order is [2, 4, 6, 0, 1, 5, 3]. The input is not sorted, so order differs from the plain index order, and ready stores i and not ptr.

At the start, time is 0 and ready is empty. The first arrival is 1. The jump step sets time to 1 at once. Without the jump, the release loop releases nothing, and heapq.heappop(ready) fails on an empty heap.

At time = 3, the release loop pushes two tasks in one pass: (4, 4) and (2, 6). The loop is a while for this reason. With an if, only one task would be released before the pop.

After task 4 ends at time = 10, ready is empty again. The next arrival is 12, so there is a second jump. Tasks 1 and 5 both arrive at 12 with duration 3. The tuples (3, 1) and (3, 5) tie on duration, so index 1 runs first. Then task 5 runs, and a third jump goes to 20 for task 3.

The result is [2, 6, 0, 4, 1, 5, 3]. A second program that scans all tasks at every step and picks the best arrived one gives the same result on 2000 random inputs.

The cost: sort once, then each task enters and leaves once

Sorting order costs O(N log N). After that, ptr only moves forward, because time never goes back. So each task is pushed once and popped once. Each push and pop on ready costs O(log N), so the loop costs O(N log N). The total is O(N log N) time and O(N) space.

The jump is what makes the loop count depend on N and not on the arrival values. Each pass of the outer while runs exactly one task. So the loop runs N times, even when an arrival is 10^9.

Example: one task [[1000000000, 1]] needs one jump and one pass. A clock that moves forward by 1 until a task arrives would take 10^9 steps for it.

On the board input, the loop makes 7 pushes, 7 pops and 3 jumps. The jumps are free of heap work. Each one is a single assignment to time.

The same shape with a different heap key

This card adds one idea to Keep the Best K: a clock decides what is in the heap. The heap key and the way the clock advances change from problem to problem. The release loop, the idle jump and the pop stay the same.

In Process Tasks Using Servers (LC 1882), the heap holds the free servers, keyed by weight and then index. A second heap holds busy servers, keyed by the time they become free. When no server is free, the clock jumps to the earliest free time.

In Meeting Rooms III (LC 2402), the free rooms are keyed by room number, and the busy rooms by the end time of their meeting. A meeting that finds no free room starts when the earliest room ends.

In IPO (LC 502), the clock is your capital. Projects whose required capital is at most your capital move into a max-heap keyed by profit. You take the best one, and your capital grows by its profit.

In Maximum Number of Events That Can Be Attended (LC 1353), the clock is the day. Events that have started go into a heap keyed by end day. Each day you attend the event with the earliest end day, after you drop events that have ended. In Minimum Interval to Include Each Query (LC 1851), sorted queries play the role of the clock, and the heap key is the interval size.

60-Second Pitch

"Only tasks that have arrived can run, so I keep a min-heap ready of the arrived tasks, keyed by (duration, index). That tuple order is exactly the CPU's rule. I sort the task indices by arrival once, and walk them with a pointer. Before each pop, I push every task whose arrival is at most time. Then I pop the root, add its duration to time, and record its index. If ready is empty, I jump time to the next arrival. Arrivals can be 10^9, so I must not tick the clock one unit at a time. The clock never goes back, so the pointer only moves forward, and each task is pushed once and popped once. The time is O(N log N) and the space is O(N)."

Common Questions

Why does the heap key have the index as a second field?

The problem says the smaller index wins when two tasks have the same duration. A tuple (duration, i) compares the duration first and the index second, so the heap applies that rule by itself. Without i, the heap would hold only durations, and it could not tell which task a duration belongs to.

What are the time and space complexity of get_order?

O(N log N) time and O(N) space. Sorting order costs O(N log N). Each task is pushed once and popped once, and each heap operation costs O(log N). The idle jump is one assignment, and there are at most N of them.

Why is a jump needed, and why not move the clock by 1?

When ready is empty, nothing can run until the next arrival, so the answer does not depend on the time in between. Arrivals go up to 10^9, so a clock that moves by 1 can take up to 10^9 steps. The jump sets time = tasks[order[ptr]][0] and keeps the loop at N passes.

What changes if a running task can be stopped when a shorter task arrives?

A task no longer runs to the end. The clock must also stop at every arrival, not only at the end of a task. At each stop, put the unfinished task back into ready with its remaining duration. This is the shortest remaining time rule, and it needs more passes than one per task.

How is this different from a max-heap of the best items, as in Top K Frequent Elements?

There, every item is in the heap from the start and you keep the best k. Here, an item enters the heap only when the clock reaches its arrival, and you pop one item each time the resource is free. The heap holds the arrived items, not the best items so far.

SWE Reasoning

Spot it

  • Each item has a time at which it becomes available: an arrival time, a start day, a capital you must reach.
  • Each time the resource is free, you serve the best item that is already available, by a fixed rule such as the shortest task or the lowest room number.
  • Something moves a clock forward: the end of a task, the end of a meeting, the next day.
  • Sorting the items by availability once is enough, because the clock never goes back.
  • Items can arrive long after the resource became free, so the resource may sit idle until the next arrival.

The invariant, step by step

  1. order lists the task indices sorted by arrival. ready is empty, time is 0 and ptr is 0.
  2. If ready is empty and the next task has not arrived, set time to its arrival. Nothing can run before then.
  3. Push (tasks[i][1], i) for every task whose arrival is at most time, and move ptr forward after each one.
  4. Now ready holds exactly the tasks that have arrived and not run. Its root is the shortest of them, with the smaller index on a tie.
  5. Pop the root, add its duration to time, and append its index to result. The task runs to the end without stopping.
  6. time only grows, so a released task never needs to leave ready until it is popped. The loop ends when result has len(tasks) entries.

Traps

  • No idle jump. When ready is empty, heapq.heappop(ready) raises an IndexError. A clock that ticks by 1 would need up to 10^9 steps.
  • < instead of <= in the release test. A task that arrives exactly at time is available. After a jump, time equals the next arrival, so nothing is released and the pop fails.
  • if instead of while in the release loop. Tasks that arrive together are released one per pass. The CPU can run a long task while a shorter task has already arrived.
  • ptr pushed in place of i. ptr is a position in order. The answer must list indices of tasks, and the tie goes to the smaller index of tasks.
  • Pushing all tasks at the start. Then the CPU can pick a task that has not arrived yet, and the result is wrong.

Practice

Canonical LeetCode:Single-Threaded CPU (LC 1834)Process Tasks Using Servers (LC 1882)Meeting Rooms III (LC 2402)Maximum Number of Events That Can Be Attended (LC 1353)IPO (LC 502)Minimum Interval to Include Each Query (LC 1851)
Theory Context•Priority Queue / Heap

Builds on Top-K Heap, which you haven’t finished yet.

P3MediumBuilt on Top-K Heap~5 min speedrun

Event Simulation with a Ready Heap (Single-Threaded CPU, Task Scheduling)

Built on
Top-K HeapA clock decides which items have arrived, and only arrived items are in the heap.
The problem

Single-Threaded CPU (LC 1834)(opens leetcode.com)

You get n tasks numbered 0 to n - 1 as a list tasks, where tasks[i] = [enqueueTime_i, processingTime_i]: task i can start from time enqueueTime_i on, and it needs processingTime_i units of time.

One CPU runs the tasks, one at a time, and each task runs to the end without a break. Whenever the CPU is free, it looks at the tasks that have already arrived and have not run yet, and picks the one with the shortest processing time; between equal times, the smaller index wins. If no task is waiting, the CPU stays idle until one arrives. A new task can start at the very moment the previous one ends.

Return the task indices in the order the CPU runs them.

Example 1
Input:tasks = [[1,2],[2,4],[3,2],[4,1]]
Output:[0,2,3,1]
At time 1 only task 0 has arrived, so it runs from 1 to 3. By then tasks 1 and 2 are waiting and task 2 is shorter, so it runs from 3 to 5. Task 3 arrived at 4 and needs 1 unit, so it beats task 1 and runs from 5 to 6; task 1 runs last, from 6 to 10.
task 0 (arrives 1)task 2 (arrives 3)task 3 (arrives 4)task 1 (arrives 2)110

Ready Heap

items with arrival times ➔ a heap of the arrived items, ordered by the rule that picks the next one ➔ pop the best; if the heap is empty, set the clock to the next arrival
Spot it when: Items become available over time (an arrival time, a start day, a capital you must reach first), and each time the resource is free you must serve the best item that is already available: the shortest task, the lightest free server, the lowest-numbered free room, the most profitable project you can afford.
Ready Heap
Python
1import heapq
2 
3def get_order(tasks: list[list[int]]) -> list[int]:
4 """Single-Threaded CPU: run the shortest arrived task next, jumping the clock when idle.
5 Time: O(N log N) | Space: O(N)
6 """
7 order = sorted(range(len(tasks)), key=lambda i: tasks[i][0]) # Task indices by arrival time
8 ready: list[tuple[int, int]] = [] # Min-heap of (duration, index) of arrived tasks; the root runs next
9 result: list[int] = []
10 time = 0
11 ptr = 0 # Next task in arrival order not yet released
12 
13 # 1. Jump the Clock When the CPU Is Idle:
14 # ready holds nothing, so the next task has not arrived yet; jumping straight to its arrival avoids ticking time forward one unit at a time.
15 while len(result) < len(tasks):
16 if not ready and time < tasks[order[ptr]][0]:
17 time = tasks[order[ptr]][0] # CPU idle: jump the clock to the next arrival
18 
19 # 2. Release Every Task That Has Arrived:
20 # Several tasks can arrive at or before the same time, so release all of them before picking, not just the next one.
21 while ptr < len(order) and tasks[order[ptr]][0] <= time:
22 i = order[ptr]
23 heapq.heappush(ready, (tasks[i][1], i)) # Release every task that has arrived by now
24 ptr += 1
25 
26 # 3. Run the Shortest Ready Task:
27 # ready holds exactly the tasks that have arrived and not run, so its root is the CPU's next pick by the shortest-duration, smaller-index rule.
28 duration, i = heapq.heappop(ready) # Shortest arrived task, smaller index on ties
29 time += duration # It runs to the end without stopping
30 result.append(i)
31 
32 return result
Invariant: Before each pop, ready holds exactly the tasks that have arrived by time and have not run yet, so ready[0] is the task the CPU starts next.

Only the tasks that have already arrived compete for the CPU, so sort the task indices by arrival once (order) and walk them with a pointer ptr: before each choice, push every task whose arrival is at most time onto the min-heap ready as (duration, i), then pop the root, run it, and add its duration to time. time never goes back, so ptr only moves forward and each task is pushed and popped exactly once; ready always holds exactly the arrived tasks that have not run, and the tuple order puts the shortest one on top, with the smaller index winning ties, which is the CPU's rule. The trap is the idle CPU: when ready is empty the next task has not arrived yet, so jump time to its arrival, tasks[order[ptr]][0], before releasing. Without the jump heapq.heappop(ready) pops an empty heap, and moving time forward one unit at a time takes up to 10^9 steps.

See it run

Ready Heap, step by step

time 0 · ptr 0

time 1 · ptr 0

time 1 · ptr 1

time 3 · ptr 1

time 3 · ptr 3

time 5 · ptr 3

time 5 · ptr 4

time 6 · ptr 4

time 10 · ptr 4

time 12 · ptr 4

time 12 · ptr 6

time 15 · ptr 6

time 18 · ptr 6

time 20 · ptr 6

time 20 · ptr 7

time 22 · ptr 7

time 22 · ptr 7 · answer [2, 6, 0, 4, 1, 5, 3]

The columns of the first two rows are positions in arrival order: the task index, then its arrival time. The ready row is the heap array of (duration, index) tuples, shown as duration,index. The last row is result.

  • ptr the next task in arrival order that is not released yet
  • jump the arrival time that time is set to while the CPU is idle
  • pushed a task that has arrived and is now in ready
  • ran the index of the task that just ran

Start: time = 0, ptr = 0 and ready is empty. order lists the task indices by arrival, and the arrival row shows each arrival time.

ready is empty and the next task arrives at 1, after time = 0. Set time to 1 at once, not one unit at a time.

With time = 1, push (2, 2) onto ready, because that task has arrived.

Pop (2, 2). Task 2 runs for 2, so time becomes 3.

With time = 3, push (4, 4) and (2, 6) onto ready, because those tasks have arrived.

Pop (2, 6). Task 6 runs for 2, so time becomes 5.

With time = 5, push (1, 0) onto ready, because that task has arrived.

Pop (1, 0). Task 0 runs for 1, so time becomes 6.

Pop (4, 4). Task 4 runs for 4, so time becomes 10.

ready is empty and the next task arrives at 12, after time = 10. Set time to 12 at once, not one unit at a time.

With time = 12, push (3, 1) and (3, 5) onto ready, because those tasks have arrived.

Pop (3, 1). Task 1 runs for 3, so time becomes 15. Task 5 has the same duration, but task 1 has the smaller index.

Pop (3, 5). Task 5 runs for 3, so time becomes 18.

ready is empty and the next task arrives at 20, after time = 18. Set time to 20 at once, not one unit at a time.

With time = 20, push (2, 3) onto ready, because that task has arrived.

Pop (2, 3). Task 3 runs for 2, so time becomes 22.

Done: all 7 tasks ran, so return [2, 6, 0, 4, 1, 5, 3].

order
02ptr
14
26
30
41
55
63
arrival
1
2
3
4
12
12
20
ready
result
1 / 17
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Not jumping time forward when the CPU is idle

    Mistake
    while len(result) < len(tasks):
    Fix
    while len(result) < len(tasks):
        if not ready and time < tasks[order[ptr]][0]:
            time = tasks[order[ptr]][0]

    What happens: When ready is empty, the next task has not arrived yet. Without the jump, the release loop releases nothing. heapq.heappop(ready) then pops an empty list and raises an IndexError. Moving time forward by 1 per step would take up to 10^9 steps, because an arrival can be 10^9. Jump time straight to tasks[order[ptr]][0].

    Example: Single-Threaded CPU (LC 1834): tasks = [[1, 2], [2, 4], [3, 2], [4, 1]]
    Mistake: raises IndexErrorFix: returns [0, 2, 3, 1]
  2. 2

    Gotcha 2: Releasing with < time instead of <= time

    Mistake
    while ptr < len(order) and tasks[order[ptr]][0] < time:
    Fix
    while ptr < len(order) and tasks[order[ptr]][0] <= time:

    What happens: A task that arrives exactly at time is already available. With <, it stays out of ready. After an idle jump, time equals the next arrival, so nothing is released. heapq.heappop(ready) then raises an IndexError.

    Example: tasks = [[1, 2], [2, 4], [3, 2], [4, 1]]
    Mistake: raises IndexErrorFix: returns [0, 2, 3, 1]
  3. 3

    Gotcha 3: Releasing with if instead of while

    Mistake
    if ptr < len(order) and tasks[order[ptr]][0] <= time:
    Fix
    while ptr < len(order) and tasks[order[ptr]][0] <= time:

    What happens: Several tasks can arrive at the same time. With if, only one of them is released before each pick. The CPU then chooses among too few tasks. It can run a long task while a shorter one has already arrived.

    Example: tasks = [[7, 10], [7, 12], [7, 5], [7, 4], [7, 2]]
    Mistake: returns [0, 1, 2, 3, 4]Fix: returns [4, 3, 2, 0, 1]
  4. 4

    Gotcha 4: Pushing ptr instead of the task's index i

    Mistake
    heapq.heappush(ready, (tasks[i][1], ptr))
    Fix
    heapq.heappush(ready, (tasks[i][1], i))

    What happens: ptr is the task's position in order, sorted by arrival. i is its index in tasks. The answer must list indices in tasks, and ties go to the smaller index in tasks. With ptr, a task that arrives first gets 0, whatever its real index.

    Example: tasks = [[2, 1], [1, 3]]
    Mistake: returns [0, 1]Fix: returns [1, 0]
Not this one if…
  • Every item is there from the start and only the best k matter, as in K Closest Points to Origin (LC 973): there is no clock, and nothing becomes available later. See Top-K Heap
  • You only need how many intervals overlap at the busiest moment, as in Meeting Rooms II (LC 253), not which item is served next or where it goes. See Sweep Line
  • An item you already took can be dropped again when a later deadline is missed, as in Course Schedule III (LC 630); here a task that has started always runs to the end. See Greedy with Regret

Deep Dive

Why only the arrived tasks are in the heap

The CPU may start a task only if the task has arrived. So at any time, the legal choices are exactly the tasks with arrival at most time that have not run. The rule of Single-Threaded CPU (LC 1834) picks the shortest duration among them, and the smaller index on a tie.

ready is a min-heap of (duration, i) tuples. Tuple order is the rule itself: it compares the duration first and the index second. So ready[0] is the task the CPU starts next, with no extra comparison code.

Tasks that have not arrived stay out of ready. They are in order, sorted by arrival, and ptr marks the first one that is not released. Before each pop, the loop moves every task with arrival at most time from order into ready. This keeps the invariant: ready holds exactly the arrived tasks that have not run.

Example: for tasks = [[1, 2], [2, 4], [3, 2], [4, 1]], the CPU starts task 0 at time 1 and finishes at 3. Tasks 1 and 2 have arrived by then, and task 2 is shorter, so it runs next and ends at 5. Task 3 has arrived at 4, so now it competes with task 1, and the shorter task 3 runs. The last is task 1. The result is [0, 2, 3, 1], which is the LeetCode answer. Task 3 is the shortest task overall, but it cannot run before it arrives.

One trace: a jump, two arrivals at once, and a tie

The board replays get_order on tasks = [[4, 1], [12, 3], [1, 2], [20, 2], [2, 4], [12, 3], [3, 2]]. Sorted by arrival, order is [2, 4, 6, 0, 1, 5, 3]. The input is not sorted, so order differs from the plain index order, and ready stores i and not ptr.

At the start, time is 0 and ready is empty. The first arrival is 1. The jump step sets time to 1 at once. Without the jump, the release loop releases nothing, and heapq.heappop(ready) fails on an empty heap.

At time = 3, the release loop pushes two tasks in one pass: (4, 4) and (2, 6). The loop is a while for this reason. With an if, only one task would be released before the pop.

After task 4 ends at time = 10, ready is empty again. The next arrival is 12, so there is a second jump. Tasks 1 and 5 both arrive at 12 with duration 3. The tuples (3, 1) and (3, 5) tie on duration, so index 1 runs first. Then task 5 runs, and a third jump goes to 20 for task 3.

The result is [2, 6, 0, 4, 1, 5, 3]. A second program that scans all tasks at every step and picks the best arrived one gives the same result on 2000 random inputs.

The cost: sort once, then each task enters and leaves once

Sorting order costs O(N log N). After that, ptr only moves forward, because time never goes back. So each task is pushed once and popped once. Each push and pop on ready costs O(log N), so the loop costs O(N log N). The total is O(N log N) time and O(N) space.

The jump is what makes the loop count depend on N and not on the arrival values. Each pass of the outer while runs exactly one task. So the loop runs N times, even when an arrival is 10^9.

Example: one task [[1000000000, 1]] needs one jump and one pass. A clock that moves forward by 1 until a task arrives would take 10^9 steps for it.

On the board input, the loop makes 7 pushes, 7 pops and 3 jumps. The jumps are free of heap work. Each one is a single assignment to time.

The same shape with a different heap key

This card adds one idea to Keep the Best K: a clock decides what is in the heap. The heap key and the way the clock advances change from problem to problem. The release loop, the idle jump and the pop stay the same.

In Process Tasks Using Servers (LC 1882), the heap holds the free servers, keyed by weight and then index. A second heap holds busy servers, keyed by the time they become free. When no server is free, the clock jumps to the earliest free time.

In Meeting Rooms III (LC 2402), the free rooms are keyed by room number, and the busy rooms by the end time of their meeting. A meeting that finds no free room starts when the earliest room ends.

In IPO (LC 502), the clock is your capital. Projects whose required capital is at most your capital move into a max-heap keyed by profit. You take the best one, and your capital grows by its profit.

In Maximum Number of Events That Can Be Attended (LC 1353), the clock is the day. Events that have started go into a heap keyed by end day. Each day you attend the event with the earliest end day, after you drop events that have ended. In Minimum Interval to Include Each Query (LC 1851), sorted queries play the role of the clock, and the heap key is the interval size.

60-Second Pitch

"Only tasks that have arrived can run, so I keep a min-heap ready of the arrived tasks, keyed by (duration, index). That tuple order is exactly the CPU's rule. I sort the task indices by arrival once, and walk them with a pointer. Before each pop, I push every task whose arrival is at most time. Then I pop the root, add its duration to time, and record its index. If ready is empty, I jump time to the next arrival. Arrivals can be 10^9, so I must not tick the clock one unit at a time. The clock never goes back, so the pointer only moves forward, and each task is pushed once and popped once. The time is O(N log N) and the space is O(N)."

Common Questions

Why does the heap key have the index as a second field?

The problem says the smaller index wins when two tasks have the same duration. A tuple (duration, i) compares the duration first and the index second, so the heap applies that rule by itself. Without i, the heap would hold only durations, and it could not tell which task a duration belongs to.

What are the time and space complexity of get_order?

O(N log N) time and O(N) space. Sorting order costs O(N log N). Each task is pushed once and popped once, and each heap operation costs O(log N). The idle jump is one assignment, and there are at most N of them.

Why is a jump needed, and why not move the clock by 1?

When ready is empty, nothing can run until the next arrival, so the answer does not depend on the time in between. Arrivals go up to 10^9, so a clock that moves by 1 can take up to 10^9 steps. The jump sets time = tasks[order[ptr]][0] and keeps the loop at N passes.

What changes if a running task can be stopped when a shorter task arrives?

A task no longer runs to the end. The clock must also stop at every arrival, not only at the end of a task. At each stop, put the unfinished task back into ready with its remaining duration. This is the shortest remaining time rule, and it needs more passes than one per task.

How is this different from a max-heap of the best items, as in Top K Frequent Elements?

There, every item is in the heap from the start and you keep the best k. Here, an item enters the heap only when the clock reaches its arrival, and you pop one item each time the resource is free. The heap holds the arrived items, not the best items so far.

SWE Reasoning

Spot it

  • Each item has a time at which it becomes available: an arrival time, a start day, a capital you must reach.
  • Each time the resource is free, you serve the best item that is already available, by a fixed rule such as the shortest task or the lowest room number.
  • Something moves a clock forward: the end of a task, the end of a meeting, the next day.
  • Sorting the items by availability once is enough, because the clock never goes back.
  • Items can arrive long after the resource became free, so the resource may sit idle until the next arrival.

The invariant, step by step

  1. order lists the task indices sorted by arrival. ready is empty, time is 0 and ptr is 0.
  2. If ready is empty and the next task has not arrived, set time to its arrival. Nothing can run before then.
  3. Push (tasks[i][1], i) for every task whose arrival is at most time, and move ptr forward after each one.
  4. Now ready holds exactly the tasks that have arrived and not run. Its root is the shortest of them, with the smaller index on a tie.
  5. Pop the root, add its duration to time, and append its index to result. The task runs to the end without stopping.
  6. time only grows, so a released task never needs to leave ready until it is popped. The loop ends when result has len(tasks) entries.

Traps

  • No idle jump. When ready is empty, heapq.heappop(ready) raises an IndexError. A clock that ticks by 1 would need up to 10^9 steps.
  • < instead of <= in the release test. A task that arrives exactly at time is available. After a jump, time equals the next arrival, so nothing is released and the pop fails.
  • if instead of while in the release loop. Tasks that arrive together are released one per pass. The CPU can run a long task while a shorter task has already arrived.
  • ptr pushed in place of i. ptr is a position in order. The answer must list indices of tasks, and the tie goes to the smaller index of tasks.
  • Pushing all tasks at the start. Then the CPU can pick a task that has not arrived yet, and the result is wrong.

Practice

Canonical LeetCode:Single-Threaded CPU (LC 1834)Process Tasks Using Servers (LC 1882)Meeting Rooms III (LC 2402)Maximum Number of Events That Can Be Attended (LC 1353)IPO (LC 502)Minimum Interval to Include Each Query (LC 1851)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•