Scheduling problems are a map of what’s easy and what’s NP-hard. On one machine, almost everything polynomial is “sort by the right key + exchange argument”. Release times, sums with weights, or several machines usually make it hard. The partitioning problems at the end are the NP-hard cousins.
The single-machine landscape
Notation : machines | constraints | objective. is completion time, lateness, tardiness, , earliness. Minimising and are the same problem (both are maximised by the same late job), but swap “max” for “sum” and the difficulty changes completely: minimising or is NP-hard even though (EDD) is easy.
Preemption often turns an NP-hard problem easy — compare each non-preemptive row above with its preemptive counterpart ( NP-hard vs. solved by preemptive EDD; NP-hard vs. SRPT; NP-hard vs. McNaughton). Preemptive EDD with release times works by running, at every moment, the available job with the earliest deadline, preempting whenever a more urgent job arrives — implemented as a priority queue keyed by deadline, processed in release-time event order. When a problem statement allows preemption, checking the preemptive row first is usually the intended solution.
| Problem | Rule | Cost |
|---|---|---|
| SPT: shortest first | ||
| Smith: ascending | ||
| EDD: earliest deadline first | ||
| Moore-Hodgson | ||
| Lawler: build the schedule backwards | ||
| preemptive EDD | ||
| SRPT: shortest remaining time | ||
| NP-hard; knapsack DP over deadlines | pseudo-poly | |
| NP-hard; Lawler’s pseudo-poly DP | ||
| , | NP-hard (non-preemptive) | |
| Johnson’s rule | ||
| , | NP-hard | |
| McNaughton: , wrap-around |
Proof: McNaughton's rule achieves the preemptive makespan lower bound
Lower bound. No machine can finish before the single longest job completes (), and the total work split across machines means some machine does at least of it (). So . Achievability. Lay all jobs end to end on a single conceptual timeline of total length , then cut this timeline into segments of length exactly each (padding the last with idle time), assigning segment to machine : a job may straddle a cut, in which case it is preempted and continues on the next machine at the same point in time. This is always valid because guarantees no job is longer than one full segment, so a job straddles at most one cut (never wraps around twice). Every machine’s segment has length exactly , so , matching the lower bound.
EDD proof: an adjacent inversion (, first) can be swapped without increasing : finishes earlier, and now finishes when did, but with a later deadline.
Proof: EDD minimises maximum lateness, and SPT minimises
EDD. Take any schedule with an adjacent inversion: jobs then with , finishing the pair at time ( at , at ). Swap them: now finishes at , so strictly decreases; now finishes at , giving . Since , , and before the swap — so ‘s new lateness is below the old maximum too. No other job’s completion time changes (the pair occupies the same total time span as before). So the swap never increases , and repeating removes every inversion, reaching EDD order without ever increasing the maximum lateness — EDD is optimal. SPT. For , take an adjacent inversion: jobs then with , with finishing at and at (both measured from the same start, being when the pair is done). Every other job’s completion time is unaffected by swapping only this pair. After swapping to then : finishes at and finishes at . The pair’s contribution to changes from to , a decrease of . So every adjacent inversion strictly decreases when swapped, meaning any schedule not already in SPT order can be strictly improved — hence SPT (no inversions left) is optimal.
Unit jobs with deadlines and profits
Maximise the total profit of on-time unit jobs. The feasible sets form a matroid, so greedy by profit works. Two implementations:
// (a) by profit descending, put each job in the latest free slot <= deadline: DSU "free slot"
iota(all(par), 0); // par over slots 0..maxD, slot 0 = none
auto find = [&](auto&& self, int x) -> int { return par[x] == x ? x : par[x] = self(self, par[x]); };
for (auto [p, d] : byProfitDesc) { int t = find(find, min(d, maxD)); if (t) total += p, par[t] = t - 1; }
// (b) by deadline ascending, take everything, drop the cheapest when count > deadline
for (auto [d, p] : byDeadline) { pq.push(p); if ((int)pq.size() > d) pq.pop(); } // min-heapFeasibility (Hall): a set is schedulable iff for every , at most of its jobs have deadline .
Moore-Hodgson (fewest late jobs)
Process by deadline, keep the on-time set; when the current total time exceeds the deadline, drop the longest job taken so far.
Proof: Moore-Hodgson maximises the number of on-time jobs
Jobs are processed by deadline order, maintaining a tentative on-time set with jobs run in deadline order among themselves (so is feasible iff its total processing time is every prefix’s deadline, checked incrementally as against the newest job’s deadline ). Claim: at each point, equals the maximum number of on-time jobs achievable using only the jobs seen so far. By induction: adding a new job either keeps (feasible, and since it’s the newest deadline, including it can only help — any optimal solution on the extended job set either includes it, matching this case, or doesn’t, in which case the old optimal size still applies and is new ) or forces . When forced, dropping the single longest job in is correct because: any feasible subset of of size (one less than ) must drop at least one job to bring the total time at or below every relevant deadline, and dropping the longest job minimizes the total time removed for a single drop — among single-job drops, this is the unique way to guarantee the remaining total time is minimized, and a smaller total time can only make later feasibility checks (against larger future deadlines) easier to satisfy, so it dominates any other single-job drop for all future steps simultaneously. Since no other way to reach size (from this one extra job) can leave a smaller remaining total time, dropping the longest job preserves optimality of the running count.
sort(all(jobs)); // by deadline
priority_queue<ll> taken; ll t = 0; // max-heap of durations
for (auto [d, p] : jobs) { taken.push(p); t += p; if (t > d) t -= taken.top(), taken.pop(); }
// on time: taken.size()The weighted version (maximise the total weight of on-time jobs) is knapsack-like: sort by deadline, = best weight finishing by time , .
The reusable pattern behind both the unit-jobs profit problem and Moore-Hodgson: process items in a natural order, greedily accept everything, and when a constraint is violated, discard the single least-useful item accepted so far (smallest profit; longest duration; whatever the constraint penalises most). A heap makes the discard . The same shape also solves “maximum items within a budget” (drop the most expensive), “attend the maximum number of events” (drop the latest-ending), and IPO/capital problems (a heap of currently-affordable projects).
Johnson’s rule (two-machine flow shop)
Every job runs on A for , then on B for . Put jobs with first in increasing , then the rest in decreasing . The two-group form is a strict weak order (the single comparator is not, with ties).
Proof: Johnson's rule minimises the makespan
The schedule processes jobs in some order, carrying a state = the time each machine becomes free; processing job updates , then . Both updates are non-decreasing functions of the incoming (sums and maxes of non-negative increments), so a smaller incoming state can never lead to a larger final makespan after processing the rest of the sequence. It suffices to show: for any adjacent pair then , swapping to then does not increase the resulting whenever (Johnson’s comparator) — then every inversion against this comparator can be removed by an adjacent swap without increasing the final makespan, so the fully sorted (two-group) order is optimal.
Write for the value of after processing then from entering state . Expanding the recurrence twice and simplifying (dropping ‘s dominated term where one candidate is always another, which direct comparison confirms) gives , and symmetrically . The first term is common to both, so write and ; the claim reduces to given . Case : then gives both (so ) and (so , i.e. ); both candidates on the left are the right-hand max. Case : then gives both (so ) and (so , i.e. ); again both left candidates are the right-hand max. Either way , proving the swap never increases , hence never increases the final makespan.Verified against a brute force over all permutations on random small instances.
sort(all(ord), [&](int i, int j) {
bool gi = a[i] <= b[i], gj = a[j] <= b[j];
if (gi != gj) return gi;
return gi ? a[i] < a[j] : b[i] > b[j];
});
ll tA = 0, tB = 0;
for (int i : ord) tA += a[i], tB = max(tB, tA) + b[i]; // tB = makespanThe same order solves “resource first spent then regained” problems (need to start, end with more or less).
Three or more machines is NP-hard in general; Johnson’s rule extends cleanly to three machines only in the special case where the middle machine is “dominated” ( or , letting the middle stage’s ordering be ignored).
Lawler (precedences, minimise max penalty)
are non-decreasing functions of completion time, and precedences form a DAG. Build from the end: at time , among jobs with no unscheduled successors, put last the one minimising ; set and repeat. Exchange argument: moving that job to the end only shifts others earlier. , or with a heap when is lateness.
Proof: Lawler's backward construction minimises
Claim: there is an optimal schedule in which the job chosen in the first step (, minimising among jobs with no unscheduled successors) is last. Let be whichever job is last in some optimal schedule . Both and are eligible to be last (no unscheduled successors — because it’s last in a valid schedule, by construction), and by the choice rule. Modify by moving to the last position and shifting every job that was between ‘s old position and the end one slot earlier (this respects precedence: removing cannot violate any predecessor’s constraint since those jobs still finish no later, and having no unscheduled successors means nothing after it depended on it). Then ‘s new cost is , every other job now finishes at or before its old completion time, and since every is non-decreasing, no other job’s cost increases. So the modified schedule has cost , is also optimal, and has last. Recursing — removing leaves a precedence-respecting problem on the remaining jobs over — shows the algorithm’s choice at every step is similarly extendable to an optimum of the remaining prefix; the one-job base case is trivial.
Parallel machines (makespan)
is NP-hard even for (partition).
| Method | Guarantee / use |
|---|---|
| list scheduling (least loaded machine) | approximation |
| LPT (longest first, then list) | , usually within a few percent of optimal in practice |
| binary search + bin-packing backtracking | exact, - with symmetry breaking |
| bitmask DP over assigned jobs | exact, |
| subset-sum bitset () | exact for - |
| meet in the middle () | exact, , huge values, |
| complete Karmarkar-Karp () | exact, , huge values |
| branch and bound with LPT as the incumbent | larger , no other exact method fits |
Backtracking for bin packing: sort jobs descending; try each job in each bin; skip bins with the same current load as a bin already tried; never open two empty bins for the same job; prune when the remaining sum can’t fit.
Bin packing heuristics: next fit has ratio 2, first fit 1.7, first-fit decreasing . FFD is the practical default.
Proof: list scheduling is a -approximation
Let be the optimal makespan, and let job (length ) be the last job assigned, placed on the machine that finishes at the overall makespan , where was that machine’s load just before was assigned. Since list scheduling always assigns to the currently least-loaded machine, was the minimum load over all machines at that instant, so every machine’s load was , giving (the sum over already-assigned jobs excludes ). So , and . Using the two standard lower bounds (the optimum’s average load) and (a single job length): .
A PTAS exists for this problem too: round job sizes, handle the “large” jobs exactly by enumeration, and fill in the small ones greedily, giving a -approximation in time for fixed — of theoretical interest only, the constants are impractical. On unrelated machines (job costs on machine , not just ), LP rounding gives a 2-approximation (Lenstra–Shmoys–Tardos); Graham’s list scheduling stays a -approximation even with precedence constraints added; and online (jobs revealed one at a time, no revisiting past assignments) greedy list scheduling is -competitive, with no online algorithm beating .
Number partitioning
Split numbers into two sets minimising the difference of sums.
| Situation | Method |
|---|---|
| - | subset-sum DP with bitset: `b |
| , huge values | meet in the middle: sort half-sums, two pointers, |
| , huge values | complete Karmarkar-Karp (branch: difference or sum the two largest, prune when the largest exceeds the rest) |
| large , approximate | Karmarkar-Karp differencing |
Karmarkar-Karp (largest differencing): repeatedly replace the two largest numbers with their difference. The last number is the discrepancy. For random inputs the expected discrepancy is , far better than greedy’s — differencing commits only to a relation (“these two end on opposite sides”) and defers the magnitude decision, so each step shrinks the residual fast instead of accumulating greedy’s error. Recover the partition by recording “these two are separated” edges and 2-colouring the resulting tree. Worst-case guarantee: none (it’s a heuristic) — but for the related makespan objective the worst-case ratio is . Random partitioning instances show a sharp easy/hard phase transition around bits per number, a well-studied phenomenon in the complexity-of-heuristics literature.
Bounded number of distinct values: subset sum with has at most distinct values, so binary splitting of multiplicities plus a bitset gives .
Other classic schedules
- Minimise total completion time on identical machines: SPT, dealt round-robin: the -th largest job overall gets multiplied by .
- Minimise the sum of waiting times with priorities: Smith’s rule again.
- Tasks with prerequisites and parallel workers, minimise time: NP-hard in general (precedence-constrained scheduling); for trees and unit times, Hu’s algorithm (highest level first) is optimal.
- CPU scheduling with arrival times, preemptive, unit weights: SRPT is optimal for .
- Interval-type fixed schedules belong to Interval Problems.