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 and Interval Scheduling, which you haven’t finished yet.

P3HardBuilt on Top-K Heap~4 min speedrun

Greedy with Regret (Heap-Based Undo)

Built on
Top-K HeapEvery item is taken first, and the most expensive taken item is removed when the limit is broken.
The problem

Course Schedule III (LC 630)(opens leetcode.com)

You are offered n online courses. courses[i] = [duration_i, lastDay_i]: course i takes duration_i days in a row, and it has to be finished no later than day lastDay_i. You start on day 1 and can only follow one course at a time, so the courses you choose run back to back.

Return the largest number of courses you can complete.

Example 1
Input:courses = [[100,200],[200,1300],[1000,1250],[2000,3200]]
Output:3
Take the 100-day course (done on day 100), then the 1000-day course (done on day 1100), then the 200-day course (done on day 1300). The 2000-day course would end on day 3300, after its last day 3200.
[100,200][1000,1250][200,1300]11300

Greedy with Regret

events sorted by deadline ➔ max-heap of durations ➔ eject longest duration
Spot it when: You choose as many items as possible under a deadline or budget, and an earlier bad choice can be undone by removing it later.
Greedy with Regret
Python
1import heapq
2 
3def schedule_course(courses: list[list[int]]) -> int:
4 """Course Schedule III: the most courses that can each finish by their last day.
5 Time: O(N log N) | Space: O(N)
6 """
7 courses.sort(key=lambda c: c[1]) # Sort by last day, not by duration
8 max_heap: list[int] = [] # Negated durations of the courses taken
9 total_time = 0
10 
11 for duration, last_day in courses:
12 # 1. Take Every Course First:
13 # Taking first and fixing later is what makes the greedy safe: no decision is final until the limit breaks.
14 total_time += duration
15 heapq.heappush(max_heap, -duration) # Take it first
16 
17 # 2. Regret the Longest Course When the Deadline Breaks:
18 # Dropping the longest course restores the deadline (it is at least as long as the new course) and leaves total_time as small as possible.
19 if total_time > last_day:
20 longest = -heapq.heappop(max_heap) # Regret: drop the longest course taken
21 total_time -= longest
22 
23 # 3. Return How Many Courses Survived:
24 # Each course stays in max_heap only while it still fits, so the heap size is the number of courses taken.
25 return len(max_heap)
Invariant: max_heap holds the most courses that can all finish by the current last_day, and of all such sets it has the smallest total_time.

Take every course in order of last_day: add its duration to total_time and push it onto max_heap, and if total_time now passes last_day, pop the longest course taken so far. Dropping the longest keeps the count the same as before this course while leaving total_time as small as possible, so every later course has the most room to fit. The trap is the sort key: sort by last_day, not by duration, or a short course with a late deadline can push out a course whose early deadline was still reachable.

See it run

Greedy with Regret on courses = [[4, 7], [5, 6], [2, 14], [4, 11], [5, 10], [4, 13]], step by step

last_day none · total_time 0 · len(max_heap) 0

last_day none · total_time 0 · len(max_heap) 0

last_day 6 · total_time 5 · len(max_heap) 1

last_day 7 · total_time 9 · len(max_heap) 2

last_day 7 · total_time 9 · len(max_heap) 2

last_day 7 · total_time 4 · len(max_heap) 1

last_day 10 · total_time 9 · len(max_heap) 2

last_day 11 · total_time 13 · len(max_heap) 3

last_day 11 · total_time 13 · len(max_heap) 3

last_day 11 · total_time 8 · len(max_heap) 2

last_day 13 · total_time 12 · len(max_heap) 3

last_day 14 · total_time 14 · len(max_heap) 4

total_time 14 · len(max_heap) 4 · answer 4

Each column in the first two rows is one course, sorted by last day. The bottom row is the array of max_heap, which stores negated durations.

  • taking the course taken at this step
  • pushed the value just pushed onto max_heap
  • longest the longest course taken, at the heap root
  • dropped a course that was removed from max_heap

The courses are sorted by last_day: [5, 6], [4, 7], [5, 10], [4, 11], [4, 13], [2, 14]. max_heap is empty.

Sorting by duration instead would end with 3 courses. Sorting by last_day ends with 4.

Take [5, 6]: total_time becomes 5 and -5 is pushed onto max_heap. 5 is at most 6.

Take [4, 7]: total_time becomes 9 and -4 is pushed onto max_heap. 9 is more than 7.

total_time 9 passes last_day 7. The longest course taken is 5 days.

heappop removes it, and total_time becomes 4. The count is the same as before this course.

Take [5, 10]: total_time becomes 9 and -5 is pushed onto max_heap. 9 is at most 10.

Take [4, 11]: total_time becomes 13 and -4 is pushed onto max_heap. 13 is more than 11.

total_time 13 passes last_day 11. The longest course taken is 5 days.

heappop removes it, and total_time becomes 8. The count is the same as before this course.

Take [4, 13]: total_time becomes 12 and -4 is pushed onto max_heap. 12 is at most 13.

Take [2, 14]: total_time becomes 14 and -2 is pushed onto max_heap. 14 is at most 14.

max_heap holds 4 courses, so schedule_course returns 4.

duration
05
14
25
34
44
52
last_day
6
7
10
11
13
14
max_heap
1 / 13
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Sorting the courses by duration instead of last_day

    Mistake
    courses.sort(key=lambda c: c[0])
    Fix
    courses.sort(key=lambda c: c[1])

    What happens: Each course is [duration, last_day], so c[0] is the duration. Shortest-first can take a short course with a late deadline first. That uses time that a course with an early deadline needed. The early course then misses its deadline, and fewer courses fit.

    Example: Course Schedule III (LC 630): courses = [[3, 3], [1, 10], [1, 10]]
    Mistake: returns 2Fix: returns 3
  2. 2

    Gotcha 2: Skipping a course that does not fit, instead of swapping

    Mistake
    if total_time + duration <= last_day:
        total_time += duration
        heapq.heappush(max_heap, -duration)
    Fix
    total_time += duration
    heapq.heappush(max_heap, -duration)
    if total_time > last_day:
        longest = -heapq.heappop(max_heap)
        total_time -= longest

    What happens: Skipping keeps a long course that was taken earlier. The blueprint takes the new course first. If total_time then passes last_day, it drops the longest course taken so far. The count stays the same, and total_time becomes as small as possible. Later courses then have more room.

    Example: courses = [[5, 5], [4, 6], [2, 6]]
    Mistake: returns 1Fix: returns 2
  3. 3

    Gotcha 3: Forgetting to negate the value popped from max_heap

    Mistake
    longest = heapq.heappop(max_heap)
    Fix
    longest = -heapq.heappop(max_heap)

    What happens: max_heap stores negated durations, so the pop returns -5 for a 5-day course. Without the minus sign, longest is -5. total_time -= longest then adds 5 days instead of removing them. Every later course looks late.

    Example: courses = [[5, 5], [4, 6], [2, 6]]
    Mistake: returns 1Fix: returns 2
  4. 4

    Gotcha 4: Returning total_time instead of the heap's size

    Mistake
    return total_time
    Fix
    return len(max_heap)

    What happens: The question asks how many courses can be taken. max_heap holds exactly the courses kept, so the answer is len(max_heap). total_time is the number of days they use.

    Example: courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]]
    Mistake: returns 1300Fix: returns 3
Not this one if…
  • Each interval has a fixed start and end, as in Non-overlapping Intervals (LC 435), so nothing can be moved earlier and there is no running total to go over. See Interval Scheduling
  • No budget or deadline can be exceeded by an earlier pick, as in Jump Game (LC 55), so a choice never has to be taken back. See Greedy

Deep Dive

Why taking every course first, then dropping the longest, is safe

Top-K Heap keeps the best k items and evicts the weakest when there are too many. This card has no fixed k. The limit is a deadline: the courses taken must finish by each last_day. The card evicts the longest taken course when the limit breaks.

Courses are sorted by last_day. Taken courses run one after another, so after the course with last_day the total time is total_time. If total_time is at most last_day, every taken course so far finishes in time, because earlier courses have earlier or equal last days and finish earlier.

If total_time is more than last_day, one course must go. Dropping the longest course taken so far gives the smallest total_time that is possible with the same number of courses. A smaller total_time leaves the most room for later courses. If the new course is the longest, it is the one dropped, and the set stays as it was.

The board uses the courses [4, 7], [5, 6], [2, 14], [4, 11], [5, 10], [4, 13]. Sorted by last_day, they are [5, 6], [4, 7], [5, 10], [4, 11], [4, 13], [2, 14]. The max_heap regrets two courses of 5 days, and schedule_course returns 4. Course Schedule III (LC 630) has the same shape.

The trap: the sort key and the skip rule

The sort key must be last_day. On the board's courses, sorting by duration ends with 3 courses, but the correct answer is 4. A short course with a late deadline is taken first, and it uses time that a course with an early deadline needed.

A second wrong idea is to skip a course that does not fit: if total_time + duration <= last_day. A skipped course can never be taken back, and a long course taken earlier stays in the schedule. On the board's courses, this rule returns 3. The blueprint takes the new course first and drops the longest course. This keeps the count and lowers total_time.

max_heap stores negated durations, because heapq is a min-heap. The pop returns a negative number. Write longest = -heapq.heappop(max_heap), then total_time -= longest. Without the minus sign, the subtraction adds the days.

The answer is len(max_heap), the number of courses kept. total_time is the number of days they use, which is a different value.

The cost, and the same idea in other problems

Sorting costs O(N log N). Each course is pushed once and popped at most once, and each heap operation costs O(log N). So the time is O(N log N) and the space is O(N).

Furthest Building You Can Reach (LC 1642) uses the same take first, regret later idea. Each climb is first paid for with a ladder. When the ladders run out, the smallest climb in the heap is paid for with bricks instead. There the heap is a min-heap, because the regret is the smallest climb.

Minimum Number of Refueling Stops (LC 871) takes every station passed, adds its fuel to a max-heap, and uses the largest fuel only when the car would otherwise stop. The heap holds choices that are made later, when they are needed.

The regret is possible only because of an exchange argument: swapping a longer taken course for a shorter new one never makes the schedule worse. A problem where a choice cannot be swapped this way, such as Non-overlapping Intervals (LC 435), is sorted by end time and has no regret step.

60-Second Pitch

"Each course has a duration and a last day. I sort the courses by last day, so the order of taking is fixed. I take every course first. I add its duration to total_time and push it onto a max-heap. If total_time passes the last day, I pop the longest course taken so far. That keeps the count the same as before this course, and it makes total_time as small as possible, so later courses have the most room. At the end, the heap size is the answer. The sort costs O(N log N), each heap operation costs O(log N), and the space is O(N)."

Common Questions

Why do you sort by last_day and not by duration?

Courses are taken one after another, so each course must finish by its own last day. If courses are in last_day order, one check of total_time against the current last_day covers every earlier course. Sorting by duration can take a short course with a late deadline first and block a course with an early deadline.

Why drop the longest course and not the new one?

The longest course uses the most days. Removing it gives the smallest total_time for the same count of courses. If the new course is the longest, the pop removes the new course itself, and the set stays as it was.

What are the time and space complexities?

O(N log N) time: the sort costs O(N log N), and each course makes one push and at most one pop at O(log N). O(N) space for max_heap.

How is this different from Non-overlapping Intervals (LC 435)?

An interval has a fixed start and end, so it cannot move earlier, and there is no running total. Sorting by end time and keeping the earliest end is enough. Here the courses have only a duration and a last day, so a taken course can be removed later to make room.

Can you prove that the greedy gives the maximum number of courses?

Use an exchange argument. Among all sets of the same size that fit, the heap keeps the one with the smallest total time. Swapping a longer course for a shorter one only lowers the total time, so no deadline breaks. Dropping the longest course therefore keeps the best set for every later course.

SWE Reasoning

Spot it

  • You choose as many items as possible under a deadline or a budget.
  • Items have a cost and a limit. Taking an item uses cost, and the total cost must stay within the limit.
  • A plain greedy choice can be wrong, but an earlier choice can be undone by removing the most expensive item taken.
  • Sorting by the limit makes the order of taking fixed, and a heap finds the most expensive taken item.

The invariant, step by step

  1. Sort courses by last_day. max_heap is empty and total_time is 0.
  2. For each course, add duration to total_time and push -duration onto max_heap. The course is taken for now.
  3. If total_time is at most last_day, every taken course finishes in time. Nothing else happens.
  4. If total_time is more than last_day, pop the longest duration from max_heap and subtract it from total_time.
  5. The count is the same as before this course, and total_time is the smallest possible for that count. So later courses have the most room.
  6. At the end, max_heap holds the most courses that fit. The answer is len(max_heap).

Traps

  • Sorting by duration. A short course with a late deadline is taken first and blocks a course with an early deadline. Sort by last_day.
  • Skipping a course that does not fit. A long course taken earlier stays. Take the new course first, and drop the longest course when the limit breaks.
  • A missing minus sign on the pop. longest is negative, and total_time -= longest adds days. Use -heapq.heappop(max_heap).
  • Returning total_time. The question asks for the number of courses. Return len(max_heap).

Practice

Canonical LeetCode:Course Schedule III (LC 630)Furthest Building You Can Reach (LC 1642)Minimum Number of Refueling Stops (LC 871)Make the Prefix Sum Non-negative (LC 2599)
Theory Context•Priority Queue / Heap

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

P3HardBuilt on Top-K Heap~4 min speedrun

Greedy with Regret (Heap-Based Undo)

Built on
Top-K HeapEvery item is taken first, and the most expensive taken item is removed when the limit is broken.
The problem

Course Schedule III (LC 630)(opens leetcode.com)

You are offered n online courses. courses[i] = [duration_i, lastDay_i]: course i takes duration_i days in a row, and it has to be finished no later than day lastDay_i. You start on day 1 and can only follow one course at a time, so the courses you choose run back to back.

Return the largest number of courses you can complete.

Example 1
Input:courses = [[100,200],[200,1300],[1000,1250],[2000,3200]]
Output:3
Take the 100-day course (done on day 100), then the 1000-day course (done on day 1100), then the 200-day course (done on day 1300). The 2000-day course would end on day 3300, after its last day 3200.
[100,200][1000,1250][200,1300]11300

Greedy with Regret

events sorted by deadline ➔ max-heap of durations ➔ eject longest duration
Spot it when: You choose as many items as possible under a deadline or budget, and an earlier bad choice can be undone by removing it later.
Greedy with Regret
Python
1import heapq
2 
3def schedule_course(courses: list[list[int]]) -> int:
4 """Course Schedule III: the most courses that can each finish by their last day.
5 Time: O(N log N) | Space: O(N)
6 """
7 courses.sort(key=lambda c: c[1]) # Sort by last day, not by duration
8 max_heap: list[int] = [] # Negated durations of the courses taken
9 total_time = 0
10 
11 for duration, last_day in courses:
12 # 1. Take Every Course First:
13 # Taking first and fixing later is what makes the greedy safe: no decision is final until the limit breaks.
14 total_time += duration
15 heapq.heappush(max_heap, -duration) # Take it first
16 
17 # 2. Regret the Longest Course When the Deadline Breaks:
18 # Dropping the longest course restores the deadline (it is at least as long as the new course) and leaves total_time as small as possible.
19 if total_time > last_day:
20 longest = -heapq.heappop(max_heap) # Regret: drop the longest course taken
21 total_time -= longest
22 
23 # 3. Return How Many Courses Survived:
24 # Each course stays in max_heap only while it still fits, so the heap size is the number of courses taken.
25 return len(max_heap)
Invariant: max_heap holds the most courses that can all finish by the current last_day, and of all such sets it has the smallest total_time.

Take every course in order of last_day: add its duration to total_time and push it onto max_heap, and if total_time now passes last_day, pop the longest course taken so far. Dropping the longest keeps the count the same as before this course while leaving total_time as small as possible, so every later course has the most room to fit. The trap is the sort key: sort by last_day, not by duration, or a short course with a late deadline can push out a course whose early deadline was still reachable.

See it run

Greedy with Regret on courses = [[4, 7], [5, 6], [2, 14], [4, 11], [5, 10], [4, 13]], step by step

last_day none · total_time 0 · len(max_heap) 0

last_day none · total_time 0 · len(max_heap) 0

last_day 6 · total_time 5 · len(max_heap) 1

last_day 7 · total_time 9 · len(max_heap) 2

last_day 7 · total_time 9 · len(max_heap) 2

last_day 7 · total_time 4 · len(max_heap) 1

last_day 10 · total_time 9 · len(max_heap) 2

last_day 11 · total_time 13 · len(max_heap) 3

last_day 11 · total_time 13 · len(max_heap) 3

last_day 11 · total_time 8 · len(max_heap) 2

last_day 13 · total_time 12 · len(max_heap) 3

last_day 14 · total_time 14 · len(max_heap) 4

total_time 14 · len(max_heap) 4 · answer 4

Each column in the first two rows is one course, sorted by last day. The bottom row is the array of max_heap, which stores negated durations.

  • taking the course taken at this step
  • pushed the value just pushed onto max_heap
  • longest the longest course taken, at the heap root
  • dropped a course that was removed from max_heap

The courses are sorted by last_day: [5, 6], [4, 7], [5, 10], [4, 11], [4, 13], [2, 14]. max_heap is empty.

Sorting by duration instead would end with 3 courses. Sorting by last_day ends with 4.

Take [5, 6]: total_time becomes 5 and -5 is pushed onto max_heap. 5 is at most 6.

Take [4, 7]: total_time becomes 9 and -4 is pushed onto max_heap. 9 is more than 7.

total_time 9 passes last_day 7. The longest course taken is 5 days.

heappop removes it, and total_time becomes 4. The count is the same as before this course.

Take [5, 10]: total_time becomes 9 and -5 is pushed onto max_heap. 9 is at most 10.

Take [4, 11]: total_time becomes 13 and -4 is pushed onto max_heap. 13 is more than 11.

total_time 13 passes last_day 11. The longest course taken is 5 days.

heappop removes it, and total_time becomes 8. The count is the same as before this course.

Take [4, 13]: total_time becomes 12 and -4 is pushed onto max_heap. 12 is at most 13.

Take [2, 14]: total_time becomes 14 and -2 is pushed onto max_heap. 14 is at most 14.

max_heap holds 4 courses, so schedule_course returns 4.

duration
05
14
25
34
44
52
last_day
6
7
10
11
13
14
max_heap
1 / 13
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Sorting the courses by duration instead of last_day

    Mistake
    courses.sort(key=lambda c: c[0])
    Fix
    courses.sort(key=lambda c: c[1])

    What happens: Each course is [duration, last_day], so c[0] is the duration. Shortest-first can take a short course with a late deadline first. That uses time that a course with an early deadline needed. The early course then misses its deadline, and fewer courses fit.

    Example: Course Schedule III (LC 630): courses = [[3, 3], [1, 10], [1, 10]]
    Mistake: returns 2Fix: returns 3
  2. 2

    Gotcha 2: Skipping a course that does not fit, instead of swapping

    Mistake
    if total_time + duration <= last_day:
        total_time += duration
        heapq.heappush(max_heap, -duration)
    Fix
    total_time += duration
    heapq.heappush(max_heap, -duration)
    if total_time > last_day:
        longest = -heapq.heappop(max_heap)
        total_time -= longest

    What happens: Skipping keeps a long course that was taken earlier. The blueprint takes the new course first. If total_time then passes last_day, it drops the longest course taken so far. The count stays the same, and total_time becomes as small as possible. Later courses then have more room.

    Example: courses = [[5, 5], [4, 6], [2, 6]]
    Mistake: returns 1Fix: returns 2
  3. 3

    Gotcha 3: Forgetting to negate the value popped from max_heap

    Mistake
    longest = heapq.heappop(max_heap)
    Fix
    longest = -heapq.heappop(max_heap)

    What happens: max_heap stores negated durations, so the pop returns -5 for a 5-day course. Without the minus sign, longest is -5. total_time -= longest then adds 5 days instead of removing them. Every later course looks late.

    Example: courses = [[5, 5], [4, 6], [2, 6]]
    Mistake: returns 1Fix: returns 2
  4. 4

    Gotcha 4: Returning total_time instead of the heap's size

    Mistake
    return total_time
    Fix
    return len(max_heap)

    What happens: The question asks how many courses can be taken. max_heap holds exactly the courses kept, so the answer is len(max_heap). total_time is the number of days they use.

    Example: courses = [[100, 200], [200, 1300], [1000, 1250], [2000, 3200]]
    Mistake: returns 1300Fix: returns 3
Not this one if…
  • Each interval has a fixed start and end, as in Non-overlapping Intervals (LC 435), so nothing can be moved earlier and there is no running total to go over. See Interval Scheduling
  • No budget or deadline can be exceeded by an earlier pick, as in Jump Game (LC 55), so a choice never has to be taken back. See Greedy

Deep Dive

Why taking every course first, then dropping the longest, is safe

Top-K Heap keeps the best k items and evicts the weakest when there are too many. This card has no fixed k. The limit is a deadline: the courses taken must finish by each last_day. The card evicts the longest taken course when the limit breaks.

Courses are sorted by last_day. Taken courses run one after another, so after the course with last_day the total time is total_time. If total_time is at most last_day, every taken course so far finishes in time, because earlier courses have earlier or equal last days and finish earlier.

If total_time is more than last_day, one course must go. Dropping the longest course taken so far gives the smallest total_time that is possible with the same number of courses. A smaller total_time leaves the most room for later courses. If the new course is the longest, it is the one dropped, and the set stays as it was.

The board uses the courses [4, 7], [5, 6], [2, 14], [4, 11], [5, 10], [4, 13]. Sorted by last_day, they are [5, 6], [4, 7], [5, 10], [4, 11], [4, 13], [2, 14]. The max_heap regrets two courses of 5 days, and schedule_course returns 4. Course Schedule III (LC 630) has the same shape.

The trap: the sort key and the skip rule

The sort key must be last_day. On the board's courses, sorting by duration ends with 3 courses, but the correct answer is 4. A short course with a late deadline is taken first, and it uses time that a course with an early deadline needed.

A second wrong idea is to skip a course that does not fit: if total_time + duration <= last_day. A skipped course can never be taken back, and a long course taken earlier stays in the schedule. On the board's courses, this rule returns 3. The blueprint takes the new course first and drops the longest course. This keeps the count and lowers total_time.

max_heap stores negated durations, because heapq is a min-heap. The pop returns a negative number. Write longest = -heapq.heappop(max_heap), then total_time -= longest. Without the minus sign, the subtraction adds the days.

The answer is len(max_heap), the number of courses kept. total_time is the number of days they use, which is a different value.

The cost, and the same idea in other problems

Sorting costs O(N log N). Each course is pushed once and popped at most once, and each heap operation costs O(log N). So the time is O(N log N) and the space is O(N).

Furthest Building You Can Reach (LC 1642) uses the same take first, regret later idea. Each climb is first paid for with a ladder. When the ladders run out, the smallest climb in the heap is paid for with bricks instead. There the heap is a min-heap, because the regret is the smallest climb.

Minimum Number of Refueling Stops (LC 871) takes every station passed, adds its fuel to a max-heap, and uses the largest fuel only when the car would otherwise stop. The heap holds choices that are made later, when they are needed.

The regret is possible only because of an exchange argument: swapping a longer taken course for a shorter new one never makes the schedule worse. A problem where a choice cannot be swapped this way, such as Non-overlapping Intervals (LC 435), is sorted by end time and has no regret step.

60-Second Pitch

"Each course has a duration and a last day. I sort the courses by last day, so the order of taking is fixed. I take every course first. I add its duration to total_time and push it onto a max-heap. If total_time passes the last day, I pop the longest course taken so far. That keeps the count the same as before this course, and it makes total_time as small as possible, so later courses have the most room. At the end, the heap size is the answer. The sort costs O(N log N), each heap operation costs O(log N), and the space is O(N)."

Common Questions

Why do you sort by last_day and not by duration?

Courses are taken one after another, so each course must finish by its own last day. If courses are in last_day order, one check of total_time against the current last_day covers every earlier course. Sorting by duration can take a short course with a late deadline first and block a course with an early deadline.

Why drop the longest course and not the new one?

The longest course uses the most days. Removing it gives the smallest total_time for the same count of courses. If the new course is the longest, the pop removes the new course itself, and the set stays as it was.

What are the time and space complexities?

O(N log N) time: the sort costs O(N log N), and each course makes one push and at most one pop at O(log N). O(N) space for max_heap.

How is this different from Non-overlapping Intervals (LC 435)?

An interval has a fixed start and end, so it cannot move earlier, and there is no running total. Sorting by end time and keeping the earliest end is enough. Here the courses have only a duration and a last day, so a taken course can be removed later to make room.

Can you prove that the greedy gives the maximum number of courses?

Use an exchange argument. Among all sets of the same size that fit, the heap keeps the one with the smallest total time. Swapping a longer course for a shorter one only lowers the total time, so no deadline breaks. Dropping the longest course therefore keeps the best set for every later course.

SWE Reasoning

Spot it

  • You choose as many items as possible under a deadline or a budget.
  • Items have a cost and a limit. Taking an item uses cost, and the total cost must stay within the limit.
  • A plain greedy choice can be wrong, but an earlier choice can be undone by removing the most expensive item taken.
  • Sorting by the limit makes the order of taking fixed, and a heap finds the most expensive taken item.

The invariant, step by step

  1. Sort courses by last_day. max_heap is empty and total_time is 0.
  2. For each course, add duration to total_time and push -duration onto max_heap. The course is taken for now.
  3. If total_time is at most last_day, every taken course finishes in time. Nothing else happens.
  4. If total_time is more than last_day, pop the longest duration from max_heap and subtract it from total_time.
  5. The count is the same as before this course, and total_time is the smallest possible for that count. So later courses have the most room.
  6. At the end, max_heap holds the most courses that fit. The answer is len(max_heap).

Traps

  • Sorting by duration. A short course with a late deadline is taken first and blocks a course with an early deadline. Sort by last_day.
  • Skipping a course that does not fit. A long course taken earlier stays. Take the new course first, and drop the longest course when the limit breaks.
  • A missing minus sign on the pop. longest is negative, and total_time -= longest adds days. Use -heapq.heappop(max_heap).
  • Returning total_time. The question asks for the number of courses. Return len(max_heap).

Practice

Canonical LeetCode:Course Schedule III (LC 630)Furthest Building You Can Reach (LC 1642)Minimum Number of Refueling Stops (LC 871)Make the Prefix Sum Non-negative (LC 2599)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•