Problem Overview: Maximizing Courses Before Their Deadlines
LeetCode 630 Course Schedule III gives you an array of courses where each course is represented as [duration, lastDay]. You must take courses one at a time in any order. A course can only be completed if you start it early enough that you finish it on or before its lastDay deadline. Courses are taken sequentially — you cannot work on two courses at once — so taking one course delays the start of all subsequent courses.
Your goal is to find the maximum number of courses you can take. You do not need to return the actual course list, just the count. This makes it a pure optimization problem: choose a subset of courses and an order to take them such that every chosen course finishes by its deadline, and the subset is as large as possible.
The challenge is that courses conflict with each other through shared time: taking a long course early can push later courses past their deadlines. Choosing which courses to take — and in what order — requires a strategy that balances duration and deadline together, not just one factor alone.
- Input: courses[i] = [duration_i, lastDay_i] — how long the course takes and its deadline
- Output: integer — maximum number of courses you can complete
- Courses are taken sequentially (no parallel enrollment)
- A course is valid only if you finish it on or before lastDay
- You can start the first course on day 1
- n up to 10^4, duration and lastDay up to 10^4
Why Pure Greedy by Duration Fails
A natural first instinct is to sort courses by duration — shortest first — and greedily take as many as possible. This maximizes the number of courses you can fit in a given window, but completely ignores deadlines. A sequence of short courses might all finish before their deadlines, but it might also cause you to miss the deadline for a critical short course that appears later in the sorted order.
Sorting by deadline alone is also insufficient. If you sort purely by deadline and greedily take every course in that order, you might include a very long course early on that consumes a huge block of time, making all subsequent courses miss their deadlines. A long course at the front can block more shorter courses than the count benefit it provides.
The fundamental issue is that duration and deadline are coupled constraints. A course with a tight deadline must be taken early, but if it has a long duration, it blocks other courses. You cannot optimize for one dimension independently — the correct strategy must account for both simultaneously.
Combine Deadline Sorting with a Heap for the Optimal Hybrid
Sorting by deadline ensures you always consider courses in the order they expire, respecting the time structure of the problem. The max-heap layer adds the retroactive swap ability: if adding a new course would exceed its deadline but its duration is shorter than the longest course already taken, swapping improves the schedule without reducing the count. This hybrid is the key insight that makes the greedy approach optimal.
The Greedy + Heap Strategy
The optimal algorithm sorts all courses by their deadline (lastDay) in ascending order. You then iterate through the sorted courses one by one, maintaining a running time counter that tracks how many days have elapsed if you were to take all courses seen so far. You also maintain a max-heap of the durations of all courses you have tentatively accepted.
For each course, you add its duration to the running time and push it onto the max-heap. If the running time now exceeds the course's deadline, you have a conflict. You resolve it by popping the maximum duration from the heap — removing the longest course accepted so far. This reduces the running time by the maximum possible amount with only one removal, maintaining the best possible schedule with the same count (or the same schedule with a shorter total time if no pop is needed).
The crucial invariant is that the heap always contains the optimal set of courses to take up to the current deadline. When you pop the maximum after an overflow, you are either removing the course you just added (if it was the longest), or swapping it for a shorter previously accepted course — in both cases the running time decreases and the count stays the same or the schedule becomes strictly better for future courses.
- 1Sort courses by lastDay ascending
- 2Initialize: time = 0, max-heap = []
- 3For each course [duration, lastDay] in sorted order:
- 4 Add duration to time; push duration onto max-heap
- 5 If time > lastDay: pop the max from the heap; subtract it from time
- 6Return the size of the heap (number of courses accepted)
Why This Works — The Exchange Argument
The correctness of this greedy strategy is proven by an exchange argument. Suppose there exists an optimal solution that differs from the greedy solution at some course. Consider the moment when the greedy algorithm makes a swap: it removes the longest course in the heap and keeps the current course (or vice versa). If the removed course had a longer duration, then replacing it with the shorter current course results in the same or lower running time for all subsequent courses, which can only help (never hurt) their feasibility.
More formally: if the greedy heap contains a set S of courses and the optimal solution contains a different set S*, and |S*| > |S|, then some course in S* is not in S. By the exchange argument, swapping a course in S for that course — using the greedy's max-heap invariant — produces a schedule that is at least as good as S with the same count. This contradicts the assumption that |S| is not optimal.
The max-heap is the mechanism that makes the exchange argument constructive: it always tells you which course to remove to free up the maximum time, and removing the maximum-duration course can only improve the feasibility of all future courses in the sorted-by-deadline order.
The Exchange Argument Is the Standard Proof for Greedy Correctness
The exchange argument works by showing that any deviation from the greedy choice can be "swapped back" to the greedy choice without making the solution worse. For Course Schedule III, the key swap is: if the optimal solution takes a longer course instead of a shorter one at some point, you can swap them and the running time at every subsequent step is no worse. This means the greedy never misses an opportunity the optimal solution would have taken.
Implementation Details
Python's heapq module implements a min-heap, but this problem requires a max-heap to efficiently pop the largest duration. The standard workaround is to negate durations when pushing: push -duration instead of duration. When popping, negate the result to recover the original positive duration. The heap invariant is preserved because negating reverses the comparison order.
Track a running integer time variable that accumulates durations. After pushing each new course and potentially popping the maximum, the heap size gives you the current count of accepted courses. There is no need to track which specific courses are in the heap — only their durations matter for the time calculation, and only the count matters for the answer.
Edge cases are handled cleanly by this approach. If a course's duration alone exceeds its deadline (duration > lastDay), it will always be pushed then immediately popped (since time > lastDay after adding it and it would be the max or swapped for a longer one). The algorithm handles this without any special-case check.
- 1Sort: courses.sort(key=lambda c: c[1]) — sort by lastDay
- 2Initialize: import heapq; heap = []; time = 0
- 3For duration, lastDay in courses:
- 4 heapq.heappush(heap, -duration) — negate for max-heap
- 5 time += duration
- 6 If time > lastDay: time += heapq.heappop(heap) — add negative = subtract max
- 7Return len(heap)
Code Walkthrough — Python and Java
Python solution: import heapq. def scheduleCourse(courses): courses.sort(key=lambda c: c[1]); heap=[]; time=0. For d,last in courses: heapq.heappush(heap,-d); time+=d; if time>last: time+=heapq.heappop(heap). Return len(heap). Time complexity: O(n log n) for the sort plus O(n log n) for n heap operations each taking O(log n). Space complexity: O(n) for the heap in the worst case where all courses are accepted.
Java solution: public int scheduleCourse(int[][] courses) { Arrays.sort(courses, (a,b)->a[1]-b[1]); PriorityQueue<Integer> pq=new PriorityQueue<>(Collections.reverseOrder()); int time=0; for(int[] c:courses){ pq.offer(c[0]); time+=c[0]; if(time>c[1]){ time-=pq.poll(); } } return pq.size(); } Java uses PriorityQueue with reverseOrder comparator for a natural max-heap, so no negation is needed. The logic is otherwise identical to the Python solution.
Both solutions run in O(n log n) time and O(n) space. The sort dominates in practice. The heap never grows beyond n elements and the total work across all heap operations is O(n log n). This is optimal for this problem — any correct solution must at minimum read all courses, and the sorting lower bound is O(n log n) for comparison-based approaches.
Python's heapq Is a Min-Heap — Negate Durations to Simulate Max-Heap
Python does not have a built-in max-heap. The heapq module always maintains a min-heap. To use it as a max-heap, push the negated value (-duration) and negate again when popping. When you do time += heapq.heappop(heap), the popped value is negative (e.g., -10 for duration 10), so adding it to time correctly subtracts the duration. This negation trick is idiomatic Python and is expected in interviews.