Job Sequencing with Deadlines
Schedule unit-length jobs with deadlines and profits to maximize total profit: take jobs in profit order and place each in the latest free slot before its deadline.
Overview
Each job takes one unit of time, has a deadline d_i (it must finish by time d_i), and a profit p_i earned only if it finishes on time. At most one job runs per time slot. Maximize total profit. The greedy: sort by profit descending; for each job, put it into the latest free slot ≤ d_i; if none exists, skip it.
Finding "latest free slot ≤ d" is a linear scan in the basic version (O(n · maxD)), or an O(α(n)) Union-Find (Disjoint Set Union) query in the optimized version where each slot points to the nearest free slot at or before it.
This problem is a matroid (the sets of jobs that can all be scheduled on time are the independent sets of a transversal matroid), which is the deep reason greedy by weight is optimal.
Intuition
A mental model before the formal terms.
You have a row of time slots and a stack of job cards sorted by pay. Take the best-paying card; slide it as far right as its deadline allows so it blocks as few future cards as possible. Take the next card and do the same. If every slot up to its deadline is taken, throw the card away — nothing cheaper is worth displacing it, and it cannot displace anything more valuable.
Placing a job at its latest legal slot keeps early slots open for jobs with tight deadlines that you have not seen yet.
How it works
- Sort jobs by profit descending.
- Let
maxDbe the largest deadline; createmaxDslots, all free. With union-find,parent[t] = tmeans slottis free;find(t)returns the latest free slot ≤t. - For each job:
t = find(d_i). Ift ≥ 1, assign the job to slottandunion(t, t−1)(slottnow redirects tot−1). Otherwise skip. - The assigned jobs are an optimal schedule; their order within the slots is a valid sequence.
Why it works
Feasibility check. A set of jobs can all meet their deadlines iff, for every t, the number of jobs with deadline ≤ t is at most t. Placing each job at its latest free slot ≤ d_i maintains this: if a job finds no free slot, the slots 1..d_i are all occupied by jobs that were placed there, and any placement of the new job would violate the count.
Exchange / matroid argument. Let G be the greedy set and O an optimal feasible set; consider the highest-profit job j in O \ G (processed in profit order). When greedy skipped j, G's jobs with deadlines ≤ d_j already filled all d_j slots, and every one of them has profit ≥ p_j. Since O contains j plus at most d_j − 1 other jobs with deadline ≤ d_j, some greedy job g with d_g ≤ d_j is not in O. Replacing j by g in O keeps feasibility and does not lower profit. Repeating turns O into G without loss, so greedy is optimal.
Union-find makes "latest free slot ≤ d" nearly constant time: after filling slot t, redirecting it to t−1 means future searches skip over filled slots in one find.
Recognition
How to tell a problem wants this.
- "Each job takes one unit of time", "deadline", "profit only if completed on time", "one job at a time".
- Maximize total profit/count of tasks that meet their individual deadlines.
- Follow-up hint: "deadlines up to 10^9" → compress slots or use a heap-based variant instead of a slot array.
Interactive visualization
Play, step, change the input. ← → and space work too.
No interactive visualization for this topic yet
Related visualizations are linked under Related.
Pseudocode
1sort jobs by profit desc2parent[t] = t for t in 0..maxD # find(t) = latest free slot <= t3for job in jobs:4 t = find(job.deadline)5 if t >= 1:6 schedule[t] = job; total += job.profit7 parent[t] = t - 1 # slot t is now taken8return totalImplementations
1from typing import NamedTuple2 3# Job sequencing with deadlines: each job takes one unit of time, has a4# deadline, and pays a profit only if it finishes by then. Take jobs in5# decreasing profit order and place each in the LATEST free slot before its6# deadline — that placement leaves the earlier slots open for later jobs.7 8 9class Job(NamedTuple):10 deadline: int11 profit: int12 13 141 · Sort by profit, highest first15def schedule_jobs(jobs: list[Job]) -> tuple[int, list[int]]:16 idx = sorted(range(len(jobs)), key=lambda i: jobs[i].profit, reverse=True)17 max_deadline = max((j.deadline for j in jobs), default=0)18 slot = [-1] * (max_deadline + 1) # slot[t] = job index scheduled at time t19 202 · For each job, scan backwards from its deadline for a free slot21 total = 022 for i in idx:23 for t in range(min(jobs[i].deadline, max_deadline), 0, -1):24 if slot[t] == -1:25 slot[t] = i26 total += jobs[i].profit27 break28 return total, slot29 30 313 · Why the LATEST free slot: filling early would consume a slot that a32# tighter-deadline job might be the only one able to use33def schedule_jobs_earliest_slot(jobs: list[Job]) -> int:34 max_deadline = max((j.deadline for j in jobs), default=0)35 used = [False] * (max_deadline + 1)36 total = 037 for j in sorted(jobs, key=lambda x: x.profit, reverse=True):38 for t in range(1, min(j.deadline, max_deadline) + 1): # WRONG direction39 if not used[t]:40 used[t] = True41 total += j.profit42 break43 return total # NOT optimal — included to show the failure44 45 464 · Union-find makes the slot search near-constant instead of O(deadline)47class SlotDsu:48 def __init__(self, n: int) -> None:49 self.parent = list(range(n + 1))50 51 def find(self, x: int) -> int:52 while self.parent[x] != x:53 self.parent[x] = self.parent[self.parent[x]]54 x = self.parent[x]55 return x56 57 585 · find(d) returns the latest free slot at or before d, or 0 if none59def schedule_jobs_fast(jobs: list[Job]) -> int:60 max_deadline = max((j.deadline for j in jobs), default=0)61 dsu = SlotDsu(max_deadline)62 total = 063 for j in sorted(jobs, key=lambda x: x.profit, reverse=True):64 t = dsu.find(min(j.deadline, max_deadline))65 if t > 0:66 total += j.profit67 dsu.parent[t] = t - 1 # that slot is now taken; point it at the previous one68 return totalsorted(range(len(jobs)), key=..., reverse=True)builds the descending-profit index permutation in one expression.max((j.deadline for j in jobs), default=0)handles the empty list without a separate check —max()on an empty iterable would raiseValueError.range(min(deadline, max_deadline), 0, -1)is the backward scan; the0stop is exclusive, so time 1 is the last slot examined.SlotDsu.finduses path halving, keeping the chain flat without recursion.dsu.parent[t] = t - 1consumes the slot, andt == 0fromfindmeans the job cannot be scheduled at all.
max(iterable, default=...)is the clean empty-safe maximum; withoutdefaultit raisesValueError.sorted(key=..., reverse=True)is clearer than negating the key, and it keeps the sort stable.range(hi, 0, -1)counts down to 1 inclusive, since the stop bound is exclusive — a frequent off-by-one when porting from a C-style loop.list(range(n + 1))is the identity parent list for the DSU.
- Calling
max()on an empty generator withoutdefault, which raises. - Writing
range(deadline, 0, -1)asrange(deadline, 1, -1), which skips slot 1 entirely. - Scanning forward for the first free slot.
- Empty-safe maximum: Python
max(..., default=0)and JS/TSreduce(..., 0)both need the explicit fallback, while a C++ loop with an initialised accumulator gets it structurally. - Descending sort: Python
reverse=True, C++ and JS/TS a flipped comparator — where flipping the operands wrongly is silent. - Backward ranges: Python
range(hi, 0, -1)has an exclusive stop that is easy to get wrong by one; C++ and JS/TS write the condition directly. - Encapsulating the union-find: C++ and Python use a struct/class with a mutable
parent, while the JS/TS factory returns a closure that must exposeparentfor the consume step — the types show the leak that the other two hide behind a member.
Complexity
Sorting plus one union-find query per job. The naive "scan down from the deadline" version is O(n · maxD).
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Unit-length tasks with individual deadlines and rewards on one machine.
- Any matroid-structured selection: "take items by weight, keep if still independent" — the same shape as Kruskal's Algorithm.
- Non-unit durations: jobs
(duration 2, deadline 2, profit 10)and two jobs(duration 1, deadline 2, profit 6)each. Profit-first takes the 10-job and fills both slots; optimum takes both 6-jobs for 12. With durations the problem is NP-hard (it contains knapsack); use 0/1 Knapsack-style DP over time. - Jobs have precedence constraints or release times — greedy-by-profit no longer forms a matroid; use topological scheduling or search.
- Minimizing lateness rather than maximizing profit: that is a different greedy (earliest deadline first, no skipping).
Alternatives
Common mistakes
- Placing each job in the earliest free slot — blocks tight-deadline jobs and loses profit.
- Off-by-one in slot indexing (deadline 1 means slot 1, not slot 0). Using slot 0 as the "no slot" sentinel avoids this.
- Forgetting to cap
maxDatn— no more thannjobs can be scheduled, so slots beyondnare never needed. - Applying the rule when durations differ.
Interview patterns
- Job Sequencing Problem (GfG classic): sort + slot array, then upgrade to union-find when asked to optimize.
- Course Schedule III (maximum courses with deadlines and durations) — a different greedy with a max-heap of taken durations, worth contrasting.
- Explain why "latest slot" matters with a two-job example.
- Number of IslandsIntermediate
- Merge IntervalsIntermediate