Master the Job Sequencing Problem — scheduling jobs with deadlines and profits to maximize total profit, greedy strategy of sorting by profit, DSU-based efficient scheduling, complete implementations, proof of correctness, and variants including minimize lateness.
Problem Statement
Given n jobs, each with:
- A deadline d[i] (must finish by this time)
- A profit p[i] (earned if completed on time)
Each job takes exactly 1 unit of time. Find the sequence that maximizes total profit.
| Jobs | (id, deadline, profit) |
| J1 | (1, d=2, p=100) |
| J2 | (2, d=1, p=19) |
| J3 | (3, d=2, p=27) |
| J4 | (4, d=1, p=25) |
| J5 | (5, d=3, p=15) |
| Greedy by profit (descending) | J1, J3, J4, J2, J5 |
| Place J1 (p=100, d=2) | schedule at slot 2. Available: [slot 1, slot 3] |
| Place J3 (p=27, d=2) | slot 2 taken, try slot 1. Available: [slot 3] |
| Place J4 (p=25, d=1) | slot 1 taken, no slot ≤ 1 available. Skip. |
| Place J2 (p=19, d=1) | slot 1 taken, skip. |
| Place J5 (p=15, d=3) | schedule at slot 3. |
| Result | J3 (slot 1), J1 (slot 2), J5 (slot 3), profit = 27+100+15 = 142 |
Efficient Implementation with DSU
The naive O(n²) approach can be improved to O(n log n) using Union-Find to quickly find the latest available slot:
class DSU:
def __init__(self, n):
self.parent = list(range(n+1))
def find(self, x):
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x):
"""Mark slot x as used, point to x-1."""
self.parent[x] = x - 1
def job_sequencing_dsu(jobs):
"""
DSU-optimized job sequencing.
Time: O(n log n) for sort + O(n α(n)) for DSU operations.
"""
jobs = sorted(jobs, key=lambda x: -x[2])
max_d = max(j[1] for j in jobs)
dsu = DSU(max_d + 1)
selected = []
total = 0
for job_id, deadline, profit in jobs:
# Find latest available slot ≤ deadline
available = dsu.find(deadline)
if available > 0:
total += profit
selected.append(job_id)
dsu.union(available) # Mark slot as used, redirect to available-1
return total, selected
jobs = [(1,2,100),(2,1,19),(3,2,27),(4,1,25),(5,3,15)]
profit, schedule = job_sequencing_dsu(jobs)
print(f"Maximum profit: {profit}") # 142
A related greedy problem: Schedule all jobs to minimize total lateness (where lateness = max(0, finish_time - deadline)).
def minimize_lateness(jobs):
"""
Schedule jobs to minimize total lateness.
Greedy: sort by deadline (earliest deadline first — EDF).
Time: O(n log n)
"""
# Sort by deadline (EDF — Earliest Deadline First)
jobs_sorted = sorted(jobs, key=lambda x: x[1])
current_time = 0
total_lateness = 0
schedule = []
for job_id, deadline, duration in jobs_sorted:
start = current_time
finish = current_time + duration
lateness = max(0, finish - deadline)
total_lateness += lateness
schedule.append((job_id, start, finish, lateness))
current_time = finish
return total_lateness, schedule
# Each job: (id, deadline, duration)
jobs = [(1,6,3),(2,8,2),(3,9,1),(4,9,4),(5,14,3),(6,15,2)]
lateness, schedule = minimize_lateness(jobs)
print(f"Total lateness: {lateness}")
for jid, start, finish, late in schedule:
print(f" Job {jid}: [{start},{finish}], lateness={late}")
Why EDF works for minimizing lateness? Exchange argument: If job i with earlier deadline d_i is scheduled AFTER job j with later deadline d_j, swapping them reduces total lateness.
Variants
Minimize Number of Late Jobs
def min_late_jobs(jobs):
"""
Maximize jobs completed on time (equivalently minimize late jobs).
Moore's algorithm: greedy by deadline, remove longest job if late.
Time: O(n log n + n²) or O(n log n) with priority queue.
"""
jobs_sorted = sorted(jobs, key=lambda x: x[1]) # By deadline
import heapq
current_time = 0
scheduled = [] # Max-heap by duration (negate)
for job_id, deadline, duration in jobs_sorted:
heapq.heappush(scheduled, (-duration, job_id, deadline))
current_time += duration
if current_time > deadline:
# Remove the longest job (it's making things late)
longest = heapq.heappop(scheduled)
current_time += longest[0] # longest[0] is negated duration
return len(scheduled), [jid for _, jid, _ in scheduled]
jobs = [(1,3,1),(2,3,1),(3,3,1),(4,3,1),(5,3,1)] # 5 jobs, all due by time 3
print(min_late_jobs(jobs)) # Only 3 jobs can be done on time
Complexity Summary
| Variant | Greedy Criterion | Time | Algorithm |
|---|
| Max profit with deadlines | Sort by profit desc | O(n²) | Linear search |
| Max profit (efficient) | Sort by profit desc | O(n log n) | DSU |
| Minimize total lateness | Sort by deadline (EDF) | O(n log n) | Simple |
| Minimize late jobs | EDF + remove longest | O(n log n) | Priority queue |
Summary
Job Sequencing:
- Greedy choice: Process jobs in decreasing profit order, assign latest available slot ≤ deadline
- Why correct: Taking the most profitable jobs first, using latest available slots preserves flexibility for lower-profit jobs
- DSU optimization:
find(d) gives latest available slot ≤ d in O(α(n)) - Minimize lateness variant: Sort by earliest deadline (EDF) — classic greedy for scheduling
*Next Lesson: Greedy Interview Problems*