Skip to content
← All posts

Course Schedule III: Maximizing Courses with Greedy + Heap

5 min read
leetcodeproblemhardgreedyheap

Imagine you are registering for classes, and every course has a duration and a hard deadline. You can only take one course at a time, and you need to finish each course before its deadline expires. How do you pick the maximum number of courses to take?

That is exactly what LeetCode 630: Course Schedule III asks. You are given an array of courses where courses[i] = [duration, deadline]. You start at time 0 and can only take courses sequentially. The goal is to maximize the total number of courses you complete.

courses = [[100,200],[200,1300],[1000,1250],[2000,3200]]sorted by deadline, greedy + heap selects 3 of 4 courses050010001500200025003000dur=100d=200dur=1000dur=200d=1300dur=2000d=3200KEEPDROPKEEPKEEP
After sorting by deadline, the greedy algorithm adds courses sequentially and uses a max-heap to drop the longest course whenever the total time exceeds a deadline. Result: 3 courses taken.

Why this problem matters

Course Schedule III is a classic example of the greedy + heap pattern. It teaches you that sometimes the greedy choice is not just about picking the best option right now, but also about being willing to undo a previous choice when a better trade-off appears.

This "swap out the worst" technique shows up in scheduling problems, resource allocation, and any scenario where you want to maximize the count of items that fit within constraints. The same pattern drives problems like IPO, where you greedily pick the most profitable project you can afford.

What makes this problem tricky is that a naive greedy approach (pick the shortest course first, or pick the earliest deadline first) does not work on its own. You need both: sort by deadline, then use a max-heap to swap out long courses when they cause you to miss a deadline.

The approach

The algorithm has two key ideas:

  1. Sort courses by deadline. Processing courses in deadline order guarantees that when you try to add a course, all previously added courses have deadlines at least as early. If the current course fits, great. If not, you can swap it with the longest course you have already taken.

  2. Use a max-heap to track durations. The heap lets you quickly find the longest course you have committed to. If adding a new course pushes your total time past its deadline, pop the longest course from the heap. This swap either reduces your total time (if the new course is shorter than what you removed) or keeps it the same. Either way, the count stays the same or stays valid. You never lose a course from your count without replacing it.

Why does swapping work? Because you sorted by deadline. Every course you already added has a deadline that is no later than the current course's deadline. If you remove the longest one and replace it with the current one (which is shorter or equal, otherwise you would not need to swap), the total time can only decrease. A shorter total time means all the earlier deadlines are still satisfied.

import heapq

def scheduleCourse(courses):
    courses.sort(key=lambda x: x[1])
    heap = []
    time = 0

    for duration, deadline in courses:
        heapq.heappush(heap, -duration)
        time += duration
        if time > deadline:
            time += heapq.heappop(heap)

    return len(heap)

Sorting by deadline is the foundation of this solution. It guarantees a key invariant: when you process course i, every course already in the heap has a deadline no later than deadline[i]. So if you swap out a longer course for a shorter one, the total time decreases and all earlier deadlines remain satisfied. Without this sort order, the swap argument falls apart.

Step-by-step walkthrough

Let's trace through the example courses = [[100,200],[200,1300],[1000,1250],[2000,3200]]. After sorting by deadline, we get [[100,200],[1000,1250],[200,1300],[2000,3200]].

Step 1: Process course [100, 200]. Push duration 100 onto the heap.

courseduration=100, deadline=200
time0 + 100 = 100
heap
100
check100 <= 200, fits

First course fits within its deadline. Add it.

Step 2: Process course [1000, 1250]. Push duration 1000 onto the heap.

courseduration=1000, deadline=1250
time100 + 1000 = 1100
heap
1000100
check1100 <= 1250, fits

Total time is still within the deadline. Both courses fit.

Step 3: Process course [200, 1300]. Push duration 200 onto the heap.

courseduration=200, deadline=1300
time1100 + 200 = 1300
heap
1000100200
check1300 <= 1300, fits

Exactly at the deadline. All three courses still fit.

Step 4: Process course [2000, 3200]. Push 2000, then pop the max (1000).

courseduration=2000, deadline=3200
time1300 + 2000 = 3300
heap
20001000100200
check3300 > 3200, overflow!
pop maxremove 1000, time = 3300 - 1000 = 2300
heap
2000200100
check2300 <= 3200, fits now

Adding the new course overflows the deadline. Drop the longest course from the heap (1000) to make room. Swapping a longer course for a shorter one never hurts.

Result: 3 courses can be completed.

len(heap) = 3 courses taken

The heap contains the durations of all courses we are taking: [2000, 200, 100]. We dropped the 1000-duration course to fit the 2000-duration course, keeping the total count at 3.

Complexity analysis

AspectComplexity
TimeO(n log n)
SpaceO(n)

Time: Sorting takes O(n log n). Then we iterate through all n courses, and each heap push/pop is O(log n). Total: O(n log n).

Space: The heap can hold up to n elements, so O(n).

Edge cases to watch for

  • Single course: If there is only one course and its duration fits within its deadline, take it. If not, return 0.
  • All courses have the same deadline: The algorithm still works. It will keep pushing shorter courses and popping longer ones to fit as many as possible before the shared deadline.
  • Duration exceeds deadline: A course like [500, 100] can never be taken. The algorithm handles this naturally. After pushing 500, time = 500 > 100, so it pops 500 right back off. The heap size does not grow.
  • Courses already sorted: No special handling needed. Sorting a sorted list is still O(n log n) in the worst case but often O(n) with Timsort.
  • All courses fit: If the total of all durations is within every deadline, the heap never needs to pop. You take everything.

The building blocks

This problem is built on two reusable pieces that CodeBricks drills independently:

1. Sort-then-greedily-process

Sorting the input by one dimension (here, deadline) to impose an order that makes the greedy choice valid:

courses.sort(key=lambda x: x[1])
for duration, deadline in courses:
    # process in deadline order

You see this pattern in interval scheduling (Meeting Rooms), activity selection, and many other greedy problems. The key insight is always the same: sorting by one property (deadline, end time, profit threshold) creates an invariant that makes your greedy decisions provably optimal.

2. Max-heap for tracking and swapping the worst element

Using a max-heap to efficiently find and remove the element that hurts you the most:

heapq.heappush(heap, -duration)
if time > deadline:
    time += heapq.heappop(heap)  # removes the largest duration

Python's heapq is a min-heap, so you negate values to simulate a max-heap. This is a common Python idiom. The heap lets you answer "what is the longest course I have committed to?" in O(1) and remove it in O(log n). Without the heap, you would need O(n) to find the maximum each time.

From understanding to recall

The logic of this solution is elegant: sort by deadline, greedily add each course, and swap out the longest one when you overflow. Reading it, it makes sense. But writing it from scratch in an interview requires remembering the sort key, the negation trick for the max-heap, the condition if time > deadline, and the fact that heappop returns the negated max.

Those details are exactly what spaced repetition drills. You practice writing the sort, the heap push, and the swap logic from memory at increasing intervals. After a few rounds, the pattern becomes automatic. You stop worrying about whether it is time > deadline or time >= deadline (it is >, because equal means it fits exactly). You just write it.

Related posts

CodeBricks breaks Course Schedule III into its sort-by-deadline and max-heap-swap building blocks, then drills them independently with spaced repetition. You type each piece from scratch until the greedy + heap pattern is automatic. When a scheduling optimization problem shows up in your interview, you do not hesitate. You just write it.