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.

ProblemRuleCost
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 deadlinespseudo-poly
NP-hard; Lawler’s pseudo-poly DP
, NP-hard (non-preemptive)
Johnson’s rule
, NP-hard
McNaughton: , wrap-around

EDD proof: an adjacent inversion (, first) can be swapped without increasing : finishes earlier, and now finishes when did, but with a later deadline.

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-heap

Feasibility (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.

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).

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 = makespan

The 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.

Parallel machines (makespan)

is NP-hard even for (partition).

MethodGuarantee / 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 backtrackingexact, - with symmetry breaking
bitmask DP over assigned jobsexact,
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 incumbentlarger , 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.

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.

SituationMethod
-subset-sum DP with bitset: `b
, huge valuesmeet in the middle: sort half-sums, two pointers,
, huge valuescomplete Karmarkar-Karp (branch: difference or sum the two largest, prune when the largest exceeds the rest)
large , approximateKarmarkar-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.