GreedyAlgorithmaka job scheduling with deadlines, unit-time job sequencing, maximum profit scheduling

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.

Pattern: Union-FindPractice (2)
Progress

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.

greedydeadlinesunion-findmatroidexchange argument

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

  1. Sort jobs by profit descending.
  2. Let maxD be the largest deadline; create maxD slots, all free. With union-find, parent[t] = t means slot t is free; find(t) returns the latest free slot ≤ t.
  3. For each job: t = find(d_i). If t ≥ 1, assign the job to slot t and union(t, t−1) (slot t now redirects to t−1). Otherwise skip.
  4. 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 desc
2parent[t] = t for t in 0..maxD # find(t) = latest free slot <= t
3for job in jobs:
4 t = find(job.deadline)
5 if t >= 1:
6 schedule[t] = job; total += job.profit
7 parent[t] = t - 1 # slot t is now taken
8return total

Implementations

1from typing import NamedTuple
2
3# Job sequencing with deadlines: each job takes one unit of time, has a
4# deadline, and pays a profit only if it finishes by then. Take jobs in
5# decreasing profit order and place each in the LATEST free slot before its
6# deadline — that placement leaves the earlier slots open for later jobs.
7
8
9class Job(NamedTuple):
10 deadline: int
11 profit: int
12
13
141 · Sort by profit, highest first
15def 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 t
19
202 · For each job, scan backwards from its deadline for a free slot
21 total = 0
22 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] = i
26 total += jobs[i].profit
27 break
28 return total, slot
29
30
313 · Why the LATEST free slot: filling early would consume a slot that a
32# tighter-deadline job might be the only one able to use
33def 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 = 0
37 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 direction
39 if not used[t]:
40 used[t] = True
41 total += j.profit
42 break
43 return total # NOT optimal — included to show the failure
44
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 x
56
57
585 · find(d) returns the latest free slot at or before d, or 0 if none
59def 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 = 0
63 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.profit
67 dsu.parent[t] = t - 1 # that slot is now taken; point it at the previous one
68 return total
Walkthrough
  1. sorted(range(len(jobs)), key=..., reverse=True) builds the descending-profit index permutation in one expression.
  2. max((j.deadline for j in jobs), default=0) handles the empty list without a separate check — max() on an empty iterable would raise ValueError.
  3. range(min(deadline, max_deadline), 0, -1) is the backward scan; the 0 stop is exclusive, so time 1 is the last slot examined.
  4. SlotDsu.find uses path halving, keeping the chain flat without recursion.
  5. dsu.parent[t] = t - 1 consumes the slot, and t == 0 from find means the job cannot be scheduled at all.
Complexity (this implementation)
time O(n log n + n * maxDeadline) scanning; O(n log n * α) with union-find · space O(maxDeadline)
Language notes
  • max(iterable, default=...) is the clean empty-safe maximum; without default it raises ValueError.
  • 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.
Common mistakes in this language
  • Calling max() on an empty generator without default, which raises.
  • Writing range(deadline, 0, -1) as range(deadline, 1, -1), which skips slot 1 entirely.
  • Scanning forward for the first free slot.
Language differences that matter here
  • Empty-safe maximum: Python max(..., default=0) and JS/TS reduce(..., 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 expose parent for the consume step — the types show the leak that the other two hide behind a member.

Complexity

Best
Average
Worst
O(n log n + n · α(n))
Space
O(n + maxD)

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

Use it when
  • 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.
Avoid it when
  • 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 maxD at n — no more than n jobs can be scheduled, so slots beyond n are 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.

Example problems