📁 Sangam Dhakal - Portfolio v2.0 - Netscape Navigator 4.0
_
□
×

📚 Interview question

What is a greedy algorithm? Give examples where greedy is optimal (interval scheduling, Dijkstra, Huffman) and where it fails (coin change with arbitrary coins, 0/1 knapsack). How do you decide whether greedy is safe in a real system?

Greedy algorithms: the greedy-choice property, classic optimal cases, counterexamples, and using greedy heuristics in production scheduling and allocation.

Mid-Level Theory Intermediate algorithms dijkstra greedy optimization scheduling

🎯 Key points

  • Greedy builds a solution by taking the locally best choice at each step and never revisiting it, usually after an O(n log n) sort or with a heap
  • It is optimal only when the problem has the greedy-choice property and optimal substructure; DP needs only optimal substructure and tries every choice
  • Provably optimal examples: interval scheduling by earliest end time, Dijkstra with non-negative weights, Kruskal/Prim MST, Huffman coding, fractional knapsack
  • Counterexamples: coins {1, 3, 4} for amount 6, 0/1 knapsack, Dijkstra with negative edges, interval scheduling by earliest start
  • Prove safety with an exchange argument, and check it by comparing with brute force on many random small inputs
  • In production greedy is often a heuristic with a known bound, such as LPT scheduling within 4/3 of the optimal makespan or first-fit decreasing for bin packing
  • Configuration can silently break a greedy assumption, for example admin-defined coin or pack sizes, or negative 'discount' edges
  • Use DP or an exact solver (such as OR-Tools) when money or correctness depends on the true optimum; otherwise document the approximation and measure the gap

⏱ 30-second answer

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.

  • Optimal: interval scheduling by earliest end time, Dijkstra (non-negative weights), Huffman coding, Kruskal/Prim, fractional knapsack.
  • Not optimal: coins {1, 3, 4} for 6, 0/1 knapsack, travelling salesman.

In production, greedy is often a good-enough heuristic (bin packing, load balancing) as long as you know it is approximate.

🗣 Full answer (senior)

How greedy works

  1. Sort or prioritise candidates by some rule.
  2. Repeatedly take the best candidate that keeps the solution valid.
  3. Never undo a choice.

Typical cost: O(n log n) for the sort (or heap), then O(n).

When greedy is optimal

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

When greedy fails

  • Coins {1, 3, 4}, amount 6: greedy 4 + 1 + 1 (3 coins), optimal 3 + 3 (2 coins).
  • 0/1 knapsack: capacity 10, items (weight 6, value 30), (5, 20), (5, 20). Best ratio first takes the 6 kg item (value 30); optimal is both 5 kg items (value 40).
  • Dijkstra with negative edges: a node's distance can still improve after it is finalised. Use Bellman-Ford.
  • Interval scheduling sorted by start time or shortest duration: wrong rule, wrong answer.

How to decide if greedy is safe

  1. Try to prove it with an exchange argument: take any optimal solution, swap in the greedy choice, show it is no worse.
  2. Try to break it with small counterexamples; a brute-force checker over random small inputs finds them quickly.
  3. If you cannot prove it, treat greedy as a heuristic: measure the gap against an exact solver on samples, and decide if it is acceptable.

Production example

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

🔍 What the interviewer is checking

Checks whether the candidate can tell when a greedy choice is provably safe versus merely convenient.

Green flags

  • Defines the greedy-choice property and optimal substructure.
  • Gives correct optimal examples (earliest end time, Dijkstra, Huffman, MST).
  • Gives counterexamples (coins {1, 3, 4}, 0/1 knapsack, negative edges).
  • Explains an exchange argument or brute-force testing.
  • Uses greedy as a heuristic in production with known trade-offs.

Red flags

  • Thinks greedy always gives the best answer.
  • Wrong ordering rule for interval scheduling.
  • Does not know Dijkstra fails with negative weights.
  • Confuses greedy with DP.

📚 Learn in depth

1. The two properties

  • Greedy-choice property: there is an optimal solution that starts with the greedy choice.
  • Optimal substructure: after making that choice, the rest is a smaller instance of the same problem.

DP needs only optimal substructure and tries all choices. Greedy bets that one choice is enough. That bet is what you must justify.

2. Exchange argument for interval scheduling

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.

3. Why Dijkstra is greedy, and why it breaks with negative edges

      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.

4. Greedy as an approximation

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.

5. Canonical coin systems

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.

6. Testing a greedy rule

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

7. Try it yourself

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.

💻 Code

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;
}

⚠ Common mistakes

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

🏛 Senior insight: trade-offs

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.

🔗 Follow-ups the interviewer may ask

  1. When: Candidate gives a greedy counterexample Why: Checks they can solve the same problem exactly with DP
    How to answer

    Dynamic programming (DP) solves a problem by combining answers to smaller subproblems, storing each answer so it is computed once. It applies when:

    1. Overlapping subproblems: the same subproblem appears many times (naive Fibonacci recomputes fib(3) repeatedly).
    2. Optimal substructure: the best answer is built from best answers to subproblems.

    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.

  2. When: Candidate mentions Dijkstra Why: Checks graph traversal basics and why BFS works only for unweighted graphs
    How to answer

    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.

  3. When: Candidate uses a priority queue for least-loaded worker Why: Checks heap internals and PriorityQueue usage
    How to answer

    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.

    • Peek min: O(1). Insert (sift up) and remove min (sift down): O(log n). Build from n items: O(n).
    • Not sorted overall, and not searchable quickly (finding an arbitrary item is O(n)).

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

🛠 Real-world problems: how you handle them

Long batch jobs piling up on one worker
Situation
A nightly batch platform distributed [400] reconciliation jobs across [8] workers using round-robin. Job durations ranged from [seconds] to [40 minutes].
Signals
The batch window overran by [2 hours] on some nights; one worker was busy for hours after the others were idle; downstream reports were late.
Investigation
Plotted per-worker total assigned duration from logs and saw a [3x] spread. Job durations were predictable from input size, so estimates were available before scheduling.
Resolution
Switched to LPT greedy: sort jobs by estimated duration descending, assign each to the least-loaded worker using a PriorityQueue keyed by total load. Kept a work-stealing fallback for estimate errors.
Prevention
Dashboards of per-worker load and makespan; alert when makespan exceeds [N] minutes; periodic recalibration of duration estimates.

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

↩ Usually asked after

More in Algorithms

Last updated 23 Oct 2026.

🌐 Sangam's Webring

⏪ Previous 🏠 Main Next ⏩
💬 Chat with me