Cairn
algorithms · greedy · O(n²)

back to Greedy

Job Sequencing with Deadlines

Given a set of jobs, each worth a profit and each due by a deadline, and a machine that can run exactly one job per unit of time starting at t=1 — every job takes exactly one time unit — pick which jobs to run, and when, to earn the most total profit. A job earns its profit only if it finishes at or before its own deadline; miss it and that job earns nothing, so there's no partial credit for starting late. This is a scheduling problem with a hard constraint (deadlines) and a soft objective (maximize profit), and unlike Activity Selection's problem shape — where every accepted item's time slot is fixed by the data — here the algorithm itself gets to choose where in the timeline an accepted job lands, and that choice turns out to be exactly as important as which jobs get accepted.

The greedy idea: sort jobs by profit, highest first, and walk the list once. For each job, look for a free slot at or before its deadline — but instead of taking the first free slot you find, scan backward from the deadline itself and take the latest free slot available. If none exists, the job is skipped; it earns nothing. This is the site's fifth entry filed under Greedy, after Huffman Coding, Activity Selection, Fractional Knapsack, and Coin Change. It's also the first of the five where the optimality proof has two independent parts rather than one: sorting by profit is only half the story, and Pitfalls below shows a case where the sort is right but the placement rule is wrong, and profit still comes out short.

Try it

Type a list of id/profit/deadline triples (comma-separated), pick a placement rule, and press Load, then Step or Run. The job chips are ordered by profit, highest first — a filled accent chip means scheduled (with the slot it landed in appended), a faded dashed chip means rejected for lack of a free slot. The slot strip below shows every time unit from 1 up to the largest deadline in the list; a slot getting a bold outline mid-step means the current job is checking whether that slot is free, and a solid green slot means a job has claimed it. The demo also brute-forces the true optimal profit for comparison (checked against a different, independent feasibility rule — see Complexity — capped at 12 jobs so that stays instant), so a rule that leaves profit on the table is visible immediately, not just claimed.

jobs, sorted by profit (descending)

time slots

Press Load.

Why it works

Two separate claims need proving, and they don't depend on each other.

Claim 1 — processing jobs highest-profit-first never costs the best job a spot. Let x be the single highest-profit job overall, with deadline d. Take any optimal schedule S. If S already runs x, done. If not, look at slot d in S: either it's empty — but then adding x there strictly improves S, contradicting optimality — or some other job y occupies it. Since x has the highest profit of any job, profit(x) ≥ profit(y), so swapping y out for x at slot d leaves every other slot in S untouched (still a valid schedule) and loses no profit. So there's always an optimal schedule that runs x, specifically at slot d. Once x is fixed there, the remaining problem — best schedule for the rest of the jobs, given slot d is now occupied — is identical in shape, so the same argument applies again to whichever job is now the highest-profit one left, and so on. Sorting once and taking a single pass is that induction unrolled.

Claim 2 — once profit order is fixed, "latest available slot" beats every other placement rule. A unit-time job doesn't care when before its deadline it runs, only that it runs by then — every slot from 1 to its deadline is equally good for the job itself. That symmetry is exactly what makes the placement choice free to optimize for the future: giving the current job the latest slot it can still use leaves every earlier slot open for whatever comes next, and an earlier slot can only ever help a job with a tighter deadline, never hurt one with a looser deadline (a looser-deadline job that could have used an early slot can just as well use a later one instead). Taking the earliest available slot instead burns exactly the flexibility a tighter-deadline job — one that hasn't been processed yet, since it's sorted later by profit — might have needed. See Pitfalls for a worked case where that costs real profit.

On the default 5-job set, sorted by profit — P1 (100, due 3), P2 (90, due 1), P3 (80, due 2), P4 (70, due 1), P5 (60, due 3) — the correct rule schedules P1 at slot 3, P2 at slot 1, and P3 at slot 2, for a total profit of 270; P4 and P5 find every slot at or before their own deadline already taken and are skipped. Brute force over all 32 subsets confirms 270 is the true maximum achievable, and confirms no 5-job schedule can do better.

Reference implementation

function jobSequencing(jobs) {
  // jobs: [{ id, profit, deadline }], deadline >= 1, all unit-time
  const sorted = [...jobs].sort((a, b) => b.profit - a.profit);
  const maxSlot = Math.max(0, ...jobs.map(j => j.deadline));
  const slots = new Array(maxSlot + 1).fill(null); // 1-indexed; slots[0] unused

  const scheduled = [];
  let totalProfit = 0;
  for (const job of sorted) {
    for (let t = Math.min(job.deadline, maxSlot); t >= 1; t--) {
      if (slots[t] === null) {
        slots[t] = job.id;
        scheduled.push({ ...job, slot: t });
        totalProfit += job.profit;
        break;
      }
    }
  }
  return { scheduled, totalProfit };
}

Pitfalls

The placement rule is not a cosmetic detail — it changes the total profit. Switch the demo's rule to earliest available slot on the unchanged default data: P1 (profit 100, due 3) grabs slot 1 the instant it's processed, since slot 1 is the first free slot at or before its own deadline. That looks harmless for P1 — it still gets scheduled — but P2 (profit 90, due 1) comes next and finds its only legal slot, slot 1, already gone. P2 is skipped entirely. The run ends with P1, P3, P5 scheduled for 240 total profit, where the correct rule (and the brute-force optimum, both shown live in the demo) reaches 270 by scheduling P1, P2, P3 instead. Nothing about the profit-descending sort order changed between the two runs — only where P1 was placed — which is exactly Claim 2 above: the earliest slot is never better than the latest one, and here it's strictly worse, because it silently spent a tight-deadline slot on a job that didn't need it yet.

The sort key matters just as much as the placement rule. Paste in the classic a/100/2, b/19/1, c/27/2, d/25/1, e/15/3 and imagine sorting by profit ascending instead of descending (this demo always sorts descending, so this is a checked-by-hand, not directly toggleable, variant): processing lowest-profit-first, e (15) claims slot 3, b (19) claims slot 1, d (25) finds slot 1 taken and is skipped, c (27) claims slot 2, and finally a (100) — the single most valuable job on the list — finds slots 1 and 2 both taken and its own deadline is 2, so it's skipped too. Total profit: 61. The correct descending order (verified in the demo by loading that same string) schedules c, a, and e for 142, confirmed optimal by brute force. Reversing the sort doesn't just reorder who goes first, it can reject the highest-profit job on the entire list.

This assumes every job takes exactly one time unit. Real scheduling problems often have jobs with different processing times, and once that's true, "does a free slot exist at or before the deadline" stops being a yes/no question answerable by scanning backward from the deadline — a job's duration determines how much contiguous room it needs, not just where it can start. That's a different, harder family of scheduling problems (weighted, variable-length interval scheduling), closer in spirit to Weighted Interval Scheduling's DP than to anything a single greedy pass over unit slots can solve.

Complexity

Time: sorting is O(n log n); the placement loop then scans, for each of n jobs, up to min(deadline, n) slots (scheduling more than n jobs is never possible, so slots beyond the n-th are never useful even if some deadline is larger), giving O(n²) worst case when deadlines are large — the dominant term over the sort. Space: O(n) for the sorted copy and the slot array.

This site's own Union-Find page builds a "find the representative, then reroute everything on the path directly at it" structure for a completely different purpose (cycle detection in Kruskal). The same reroute-on-find trick, repurposed to mean "find the nearest free slot at or before this deadline, then remember it," turns the O(n²) scan above into a near-linear O(n α(n)) one — left out of this demo, since the plain backward scan is easier to watch happen one slot at a time, but worth knowing the naive version isn't the fastest one possible.

The demo's brute-force optimal-profit check, shown purely for comparison, tries all 2^n subsets and tests each for feasibility independently of the greedy algorithm itself — a subset is schedulable if and only if, for every deadline t, at most t of its jobs have a deadline of t or earlier (a counting rule, not a simulation of any placement order) — capped at 12 jobs for exactly that reason; nothing about the greedy algorithm needs it.

See Choosing a Greedy Strategy for how this entry's proof compares against the site's other nine Greedy entries — short version: this is Tier 1, exact on every input, but the only one needing two independent exchange arguments (which jobs run, then where they land) instead of one.