Greedy algorithms: the greedy-choice property, classic optimal cases, counterexamples, and using greedy heuristics in production scheduling and allocation.
A greedy algorithm builds a solution step by step, always taking the locally best choice and never revisiting it. It is fast and simple, but only optimal when the problem has the greedy-choice property (a locally best choice is part of some optimal solution) and optimal substructure.
In production, greedy is often a good-enough heuristic (bin packing, load balancing) as long as you know it is approximate.
Typical cost: O(n log n) for the sort (or heap), then O(n).
| Problem | Greedy rule | Why it works |
|---|---|---|
| Max number of non-overlapping meetings | pick the meeting that ends earliest | leaves the most room for the rest (exchange argument) |
| Shortest paths, non-negative weights (Dijkstra) | expand the closest unvisited node | its distance cannot improve later |
| Minimum spanning tree (Kruskal, Prim) | cheapest edge that does not form a cycle | cut property |
| Huffman coding (used in DEFLATE/gzip) | merge the two least frequent symbols | provably optimal prefix code |
| Fractional knapsack | highest value per kg first | you can take fractions |
| Coins in AUD/USD/EUR denominations | largest coin first | these "canonical" systems happen to work |
A job scheduler assigned incoming batch jobs to workers. The first version used round-robin, and long jobs piled up on the same worker. We switched to a greedy rule: assign each job to the worker with the least total queued work (a min-heap keyed by load). It is not optimal (multiprocessor scheduling is NP-hard), but sorting jobs longest-first and then assigning greedily (LPT) is guaranteed to be within 4/3 of optimal makespan, and the tail completion time dropped by about 40%.
Checks whether the candidate can tell when a greedy choice is provably safe versus merely convenient.
Green flags
Red flags
DP needs only optimal substructure and tries all choices. Greedy bets that one choice is enough. That bet is what you must justify.
meeting start end
A 0 2
B 1 4
C 3 5
D 5 7
E 0 7
sorted by end: A(2), B(4), C(5), D(7), E(7)
take A -> lastEnd = 2
B starts 1 < 2 -> skip
take C (3 >= 2) -> lastEnd = 5
take D (5 >= 5) -> lastEnd = 7
E starts 0 < 7 -> skip
result: A, C, D (3 meetings)
timeline 0 1 2 3 4 5 6 7
A |=======|
C |=======|
D |=======|
Proof sketch: let O be any optimal schedule and let its first meeting be X. The greedy pick G ends no later than X, so replacing X with G keeps everything else valid. Repeat for the rest. So greedy is at least as good as O.
Compare the wrong rules: earliest start picks E first (0–7) and gets only 1 meeting; shortest duration can pick a short meeting that overlaps two others.
1
S ------> A
| ^
4| | -5
v |
B --------+
Dijkstra finalises A at distance 1 (closest), but S → B → A = 4 + (−5) = −1 is shorter. With only non-negative weights, any later path to A is at least as long as the current closest frontier, so finalising is safe.
Many real problems are NP-hard, so greedy is used with known bounds:
| Problem | Greedy heuristic | Guarantee |
|---|---|---|
| Multiprocessor scheduling (makespan) | longest job first to least-loaded worker (LPT) | ≤ 4/3 × optimal |
| Bin packing (containers onto nodes) | first-fit decreasing | ≤ 11/9 × OPT + 6/9 bins |
| Set cover (choose fewest feature flags/regions) | pick the set covering most uncovered items | ln n factor |
Kubernetes' scheduler scores nodes and places each pod greedily on the best-scoring node; it does not re-plan the cluster globally.
Greedy change-making is optimal for the Australian set {5c, 10c, 20c, 50c, $1, $2, ...} because the system is canonical. It fails for arbitrary denominations like {1, 3, 4}. If a business lets admins configure denominations (loyalty points bundles, pack sizes), you cannot assume greedy works; use DP.
// Randomised check: greedy vs exact on small inputs
var rnd = new Random(42);
for (int trial = 0; trial < 10_000; trial++)
{
var coins = new[] { 1 }.Concat(Enumerable.Range(0, 3).Select(_ => rnd.Next(2, 15))).Distinct().ToArray();
int amount = rnd.Next(1, 60);
if (GreedyCoins(coins, amount) != DpCoins(coins, amount))
{
Console.WriteLine($"Counterexample: coins [{string.Join(',', coins)}], amount {amount}");
break;
}
}
A counterexample shows up almost immediately.
Implement interval scheduling three ways (earliest start, shortest duration, earliest end), generate 1,000 random sets of 8 meetings, and compare each with brute force over all subsets. Only earliest end matches brute force every time.
public sealed record Meeting(string Name, TimeOnly Start, TimeOnly End);
// Interval scheduling: max non-overlapping meetings. O(n log n), optimal.
public static List<Meeting> MaxMeetings(IEnumerable<Meeting> meetings)
{
var chosen = new List<Meeting>();
TimeOnly lastEnd = TimeOnly.MinValue;
foreach (var m in meetings.OrderBy(m => m.End)) // greedy rule: earliest end
if (m.Start >= lastEnd)
{
chosen.Add(m);
lastEnd = m.End;
}
return chosen;
}
// Greedy load balancing (LPT): longest job first to least-loaded worker. Heuristic, <= 4/3 optimal.
public static Dictionary<int, List<Job>> Assign(IEnumerable<Job> jobs, int workers)
{
var load = new PriorityQueue<int, TimeSpan>(); // worker id by total assigned time
var result = new Dictionary<int, List<Job>>();
for (int w = 0; w < workers; w++) { load.Enqueue(w, TimeSpan.Zero); result[w] = new(); }
foreach (var job in jobs.OrderByDescending(j => j.EstimatedDuration))
{
load.TryDequeue(out var worker, out var current);
result[worker].Add(job);
load.Enqueue(worker, current + job.EstimatedDuration);
}
return result;
}
public sealed record Job(string Id, TimeSpan EstimatedDuration);
// Greedy change: only correct for canonical coin systems
public static int GreedyCoins(int[] coins, int amount)
{
int count = 0;
foreach (var c in coins.OrderDescending())
{
count += amount / c;
amount %= c;
}
return amount == 0 ? count : -1;
}
Mistake: Assuming greedy is optimal because it worked on the examples you tried. Why it is wrong: Counterexamples are often small but specific, such as coins {1, 3, 4} with amount 6 (greedy gives 3 coins, the optimum is 2). Instead: Prove it with an exchange argument, or test against brute force on thousands of random small inputs.
Mistake: Choosing the wrong greedy rule, such as earliest start or shortest meeting first. Why it is wrong: Only earliest end time is provably optimal for maximising the number of non-overlapping meetings. Instead: Justify the specific ordering, not just "be greedy".
Mistake: Running Dijkstra on a graph with negative edge weights. Why it is wrong: A distance that Dijkstra has finalised can later be improved through a negative edge, so results are wrong. Instead: Use Bellman-Ford, or Johnson's algorithm (reweight with Bellman-Ford, then run Dijkstra) for all-pairs paths.
Mistake: Treating 0/1 knapsack like fractional knapsack. Why it is wrong: Value-per-weight ordering is optimal only when items can be split. Instead: Use DP for 0/1 choices when the capacity is small enough, or a solver.
Mistake: Calling a greedy heuristic "optimal" in a design document. Why it is wrong: Stakeholders then rely on a guarantee that does not exist. Instead: Say it is a heuristic and give the known bound (for example 4/3 for LPT) or the measured gap against an exact solution.
Mistake: Hard-coding greedy change-making when denominations are configurable. Why it is wrong: An admin can configure a non-canonical set and break it without any code change. Instead: Use DP, or validate the configured set against DP when it is saved.
A senior engineer uses greedy deliberately. It is often the right engineering choice even when it is not optimal, because it is fast, explainable and incremental. Schedulers, load balancers and autoscalers make greedy decisions every second; a globally optimal solver would be too slow and too fragile for that loop.
| Situation | Choice | Trade-off |
|---|---|---|
| Greedy-choice property can be proved | Greedy | Optimal and usually O(n log n) |
| NP-hard, fast online decisions needed | Greedy heuristic with a known bound | Fast; within a stated factor of optimal |
| Exact optimum matters, state space small | DP | Exact; time and memory grow with states |
| Exact optimum matters, complex constraints | ILP or constraint solver (OR-Tools) | Exact or near-exact; slower, harder to explain |
| Inputs change all the time | Greedy plus periodic rebalancing | Simple now, corrected later |
The senior part is honesty: knowing whether the problem has the greedy-choice property, quoting the approximation bound or measuring the gap, and switching to exact methods when money or correctness depends on it. They also watch for configuration that silently breaks a greedy assumption, such as custom denominations or a negative "discount" edge in a routing graph.
Dynamic programming (DP) solves a problem by combining answers to smaller subproblems, storing each answer so it is computed once. It applies when:
fib(3) repeatedly).Two styles: memoisation (recursion plus a cache, top-down) and tabulation (fill a table in order, bottom-up, no recursion). Minimum coins: dp[a] = 1 + min(dp[a - c]) over coins c, O(amount × coins) time.
Graph traversal means visiting every node reachable from a start node exactly once, using a visited set so cycles do not loop forever. The two basic orders are BFS (breadth-first, uses a queue) and DFS (depth-first, uses a stack or recursion).
BFS visits nodes in layers of distance 0, 1, 2, ... so the first time it reaches a node is via the fewest edges: it finds shortest paths in unweighted graphs. O(V + E) time, O(V) space. Uses: degrees of separation, nearest available item, impact of a change in a dependency graph, web crawling by depth.
A heap is a complete tree stored in an array that keeps the heap property: in a min-heap every parent ≤ its children, so the smallest item is always at the root.
.NET's PriorityQueue<TElement,TPriority> (.NET 6+) is an array-backed min-heap (4-ary internally). Enqueue, Dequeue, TryPeek, EnqueueDequeue; not stable for equal priorities; no decrease-key (Remove was added in .NET 9). For a max-heap, pass a reversed comparer.
Top K from a stream: keep a min-heap of size K; for each item, if it beats the heap's minimum, replace it. That is O(n log K) time and O(K) memory.
(Not to be confused with the managed heap, which is memory.)
How to tell it (STAR)
At [company] our nightly batch kept overrunning. I measured per-worker load and found round-robin had stacked long jobs on the same workers. I replaced it with a greedy longest-job-first scheduler using a priority queue of worker loads. I was clear with the team it is a heuristic, within 4/3 of optimal, which was more than good enough. The batch window dropped from [6 hours] to [3.5 hours].
Last updated 23 Oct 2026.