Verifying a Greedy Strategy

intermediate25 min

The idea

A greedy algorithm makes the choice that looks best right now and never reconsiders. Sort the items sensibly, walk through them once, take what you can.

When it works, it is short and fast. The danger is that it is also short and fast when it is wrong — a greedy solution that fails on some inputs looks exactly like one that always succeeds. Verifying the strategy matters more than implementing it.

A case where greedy works

You have coins of value 1, 5, 10, and 25. Make a target amount using as few coins as possible.

Take as many of the largest coin as possible, then the next largest, and so on.

int[] coins = {25, 10, 5, 1};
int remaining = target;
int used = 0;

for (int c : coins) {
    used += remaining / c;
    remaining %= c;
}

System.out.println(used);

This is correct for this specific coin set. It is not correct in general. With coins of 1, 3, and 4, making 6 greedily takes 4 + 1 + 1 = three coins, while 3 + 3 = two coins is better.

The lesson from the coin example

Greedy correctness depends on the specific numbers, not on the shape of the problem. “Coin change” is not a greedy problem or a DP problem — it depends entirely on the coin set.

When the problem gives you arbitrary values from the input, assume greedy is wrong until you can argue otherwise. When it fixes the values, check whether greedy happens to work for those.

A case where greedy is provably right

Given N intervals, select the largest number of them that do not overlap.

Sort by end time, then repeatedly take the first interval that starts after the last one you took.

static class Interval {
    int start, end;
}

Arrays.sort(iv, (x, y) -> Integer.compare(x.end, y.end));

int taken = 0;
int lastEnd = Integer.MIN_VALUE;

for (Interval x : iv) {
    if (x.start >= lastEnd) {
        taken++;
        lastEnd = x.end;
    }
}

System.out.println(taken);

Sorting by end time rather than start time or duration is the entire insight, and it has a clean argument behind it: among all intervals compatible with what you have chosen so far, the one that finishes earliest leaves the most room for everything after it. Choosing it can never make the situation worse, so there is always an optimal solution that starts with that choice.

That form of argument — taking the greedy choice never rules out an optimal answer — is the standard way to justify a greedy algorithm. It is called an exchange argument.

Sorting by the wrong key

For interval scheduling, two natural-sounding alternatives are both wrong:

  • By start time: one interval starting at 0 and ending at 100 blocks everything.
  • By duration: a short interval in the middle can block two longer ones that together are better.

If you cannot articulate why your sort key is the right one, that is a signal to test the alternatives against a brute force.

How to check a greedy strategy

Before trusting it

  1. Try to build a counterexample. Small inputs, three or four elements. Deliberately look for the case where the locally best choice costs you later.
  2. Attempt an exchange argument. Can you show that swapping any optimal solution’s first choice for the greedy one keeps it optimal?
  3. Stress test against brute force. Write the exponential correct solution, generate small random inputs, compare. This is the most reliable check and it takes a few minutes. See Debugging Under Time Pressure.
  4. Check the constraints. If N ≤ 20, the problem probably wants complete search or DP, not greedy. Large N with a simple-sounding question is more often the greedy signal.

Step 3 is the one to actually do. An exchange argument you half-believe is less convincing than ten thousand random tests that agree.

Common greedy shapes

Patterns that recur

Problem shapeGreedy rule
Select the most non-overlapping intervalsSort by end time, take the earliest that fits
Cover all points with fewest fixed-length segmentsPlace each segment starting at the leftmost uncovered point
Minimize total waiting timeServe the shortest task first
Pair items to minimize the largest pair sumSort, then pair the smallest with the largest
Fit items into bins, minimizing countSort descending, place each in the first bin that fits
Maximize value with a fractional capacityTake highest value-per-unit first

The last row has an important caveat. Taking the best value-per-unit ratio is correct when items can be split, and incorrect when they cannot — the indivisible version is the knapsack problem, which needs DP. See Knapsack.

Worked example

N cows each need a stall for the interval [start, end]. Find the minimum number of stalls needed so no two cows share a stall at the same time.

The answer is the maximum number of intervals overlapping at any single moment. Sweep through events rather than reasoning about assignments:

int[] starts = new int[n];
int[] ends = new int[n];
// ... read, then sort each array independently ...
Arrays.sort(starts);
Arrays.sort(ends);

int stalls = 0, most = 0;
int i = 0, j = 0;

while (i < n) {
    if (starts[i] < ends[j]) {
        stalls++;
        most = Math.max(most, stalls);
        i++;
    } else {
        stalls--;
        j++;
    }
}

System.out.println(most);

Sorting the start and end times separately is what makes this work — you no longer care which start goes with which end, only how many intervals are open at once.

Whether the comparison is < or <= decides whether a cow leaving exactly when another arrives can reuse the stall. The problem statement determines that, and getting it backwards is wrong on precisely the inputs where times coincide.

Common mistakes

  • Assuming greedy works because the problem sounds simple. Stress test.
  • Sorting by the wrong key. Justify the key or test the alternatives.
  • Wrong strictness at boundaries< versus <= where values coincide.
  • Applying fractional greedy to an indivisible problem. That is knapsack.
  • Not resetting state between test cases when the input contains several.

Practice

For each, state the greedy rule, then try to break it before implementing.

  1. Given N tasks with durations, order them to minimize the total time each task waits before starting.
  2. Given N points on a line and a segment length L, find the fewest segments needed to cover every point.
  3. Given N values, pair them up to minimize the largest sum of any pair.
  4. Given N cows with heights and a fence of height H, find the maximum number of cows that can be stacked so that no cow supports more than its own height in total.
  5. Given N items with weights and a truck capacity C, find the minimum number of trips if each trip can carry any subset within capacity.
Hints
  1. Shortest first. The exchange argument: swapping an adjacent out-of-order pair never increases the total.
  2. Leftmost uncovered point starts the next segment, which then covers everything up to that point plus L.
  3. Sort, then pair first with last, second with second-to-last. Try to construct a counterexample — you will not find one, and seeing why is the point.
  4. Greedy on a sorted order is tempting but the right order is not obvious. Try sorting by height, then by capacity, then by the sum, and stress test each against brute force for small N.
  5. This one is a trap. It is bin packing, which is NP-hard — greedy gives a good answer but not always the optimum. If N is small the intended solution is complete search or bitmask DP, not greedy. Check the constraint.

Next