Chapter 21: Greedy Algorithms and Exchange Arguments
21.04Intermediate5 topics19 min5 interactive animations

Resource allocation

Greedy resource allocation: job sequencing with deadlines, fractional knapsack, minimum platforms, gas station and assigning cookies.

Learning goals

  • Schedule unit jobs with deadlines for maximum profit
  • Solve the fractional knapsack by value density
  • Compute minimum platforms with sorted arrivals and departures
  • Find the unique start of the gas-station circuit in one pass
  • Match supply and demand greedily with two pointers

Prerequisites

Interval schedulingSorting algorithms

Scarce resources (time slots, capacity, fuel, cookies) and many demands: greedy gives the best allocation when the demands can be ranked consistently.

21.04.1

Job sequencing with deadlines

Each job takes one unit of time, has a deadline and a profit, and only one job runs per slot. Maximise total profit. Sort jobs by **profit descending** and place each in the **latest free slot at or before its deadline**; skip it if no such slot is free. Placing late keeps earlier slots open for jobs with tighter deadlines.

The “Classic” preset (deadline:profit pairs 2:100, 1:19, 2:27, 1:25, 3:15) earns 142 from 3 jobs: profit 100 takes slot 2, 27 takes slot 1, the jobs with deadline 1 find no slot, and 15 takes slot 3. “All want slot 1” shows that only the most profitable job fits.

The slot search here is a simple scan, so the worst case is O(n · maxDeadline). A union-find structure over slots reduces the search to nearly constant time per job; that structure is covered later in the course.

Java
1// jobs[i] = {deadline, profit}; returns the maximum total profit
2static int jobSequencing(int[][] jobs) {
3 int[][] a = jobs.clone();
4 Arrays.sort(a, (x, y) -> Integer.compare(y[1], x[1])); // most profitable first
5 int maxDeadline = 0;
6 for (int[] j : a) maxDeadline = Math.max(maxDeadline, j[0]);
7 boolean[] taken = new boolean[maxDeadline + 1]; // slots 1..maxDeadline
8 int total = 0;
9 for (int[] j : a) {
10 for (int slot = j[0]; slot >= 1; slot--) { // latest free slot not after the deadline
11 if (!taken[slot]) { taken[slot] = true; total += j[1]; break; }
12 }
13 }
14 return total;
15}

Best

O(n log n)

Average

O(n log n + n·D)

Worst

O(n log n + n·D)

Space

O(D) for the slot array, D = largest deadline

Job sequencing with deadlines

Interactive · edit the input

Your input

Try

jobs by profit

2:100J1
2:27J3
1:25J4
1:19J2
3:15J5

time slots

freeslot 1
freeslot 2
freeslot 3
Setup

Every job takes one slot and must finish by its deadline. Take jobs in order of profit and give each the latest free slot on or before its deadline, which leaves earlier slots free for jobs with tighter deadlines.

Step 1 / 10
Java
1static int maxProfit(int[][] jobs) { // jobs[i] = {deadline, profit}, each takes one time slot
2 Arrays.sort(jobs, (a, b) -> Integer.compare(b[1], a[1])); // most profitable first
3 int maxDeadline = Arrays.stream(jobs).mapToInt(j -> j[0]).max().getAsInt();
4 boolean[] taken = new boolean[maxDeadline + 1]; int profit = 0;
5 for (int[] j : jobs) {
6 for (int slot = j[0]; slot >= 1; slot--) { // latest free slot on or before the deadline
7 if (!taken[slot]) { taken[slot] = true; profit += j[1]; break; }
8 }
9 }
10 return profit;
11}

Live state

jobs

5

profit

0

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L2jobs=5 profit=0Every job takes one slot and must finish by its deadline. Take jobs in order of profit and give each the latest free slot on or before its deadline, which leaves earlier slots free for jobs with tighter deadlines.
2L7job=J1 slot=2 profit=100Schedule J1 in slot 2: profit 100.
3L6job=J3 slot=2Slot 2 is taken: try the one before it.
4L7job=J3 slot=1 profit=127Schedule J3 in slot 1: profit 127.
5L6job=J4 slot=1Slot 1 is taken: try the one before it.
6L6job=J4 profit=127No free slot on or before 1: J4 is dropped.
7L6job=J2 slot=1Slot 1 is taken: try the one before it.
8L6job=J2 profit=127No free slot on or before 1: J2 is dropped.
9L7job=J5 slot=3 profit=142Schedule J5 in slot 3: profit 142.
10L10return=142Total profit 142 from 3 jobs.
21.04.2

Fractional knapsack

Items have weights and values and the thief may take **any fraction** of an item. Sort by value per unit weight, take whole items from the best down, and take a fraction of the first item that does not fit. The “Classic (capacity 50)” preset (weight:value pairs 10:60, 20:100, 30:120) takes the first two whole and two thirds of the third: 60 + 100 + 80 = 240.

This succeeds because the exchange argument works: if the bag holds some of a worse-ratio item while a better-ratio item is not fully taken, swapping a little weight from the worse to the better item raises the value. With whole items only (0/1 knapsack) the swap may be impossible, and greedy fails; that problem is solved by dynamic programming later in this part.

Java
1// items[i] = {weight, value}
2static double fractionalKnapsack(int[][] items, int capacity) {
3 int[][] a = items.clone();
4 Arrays.sort(a, (x, y) -> Double.compare((double) y[1] / y[0], (double) x[1] / x[0]));
5 double value = 0;
6 int room = capacity;
7 for (int[] it : a) {
8 if (room == 0) break;
9 int take = Math.min(room, it[0]); // whole item, or whatever room is left
10 value += (double) it[1] * take / it[0];
11 room -= take;
12 }
13 return value;
14}

Best

O(n log n)

Average

O(n log n)

Worst

O(n log n)

Space

O(n) for the sorted copy

Fractional knapsack by value per weight

Interactive · edit the input

Your input

Try

items by value/weight

10:606.00/kg
20:1005.00/kg
30:1204.00/kg

taken (weight)

0#1
0#2
0#3
Setup

Capacity 50. Items may be split, so always take the item with the best value per unit of weight first.

Step 1 / 5
Java
1static double maxValue(int[] w, int[] v, int capacity) {
2 Integer[] idx = new Integer[w.length]; for (int i = 0; i < idx.length; i++) idx[i] = i;
3 Arrays.sort(idx, (a, b) -> Double.compare((double) v[b] / w[b], (double) v[a] / w[a])); // best value per weight first
4 double value = 0; int room = capacity;
5 for (int i : idx) {
6 if (room == 0) break;
7 int take = Math.min(room, w[i]); // the whole item, or the fraction that fits
8 value += (double) v[i] * take / w[i];
9 room -= take;
10 }
11 return value;
12}

Live state

capacity

50

room

50

value

0

No condition evaluated on this step
Execution trace (dry run table)Show 5 steps
StepLineStateWhat happens
1L3capacity=50 room=50 value=0Capacity 50. Items may be split, so always take the item with the best value per unit of weight first.
2L6item=#1 taken=10 room=40 value=60Take all of item #1 (10 weight, value 60). Room left: 40.
3L6item=#2 taken=20 room=20 value=160Take all of item #2 (20 weight, value 100). Room left: 20.
4L6item=#3 taken=20 room=0 value=240Only 20 of item #3's 30 weight fits: take that fraction for value 80.00. The knapsack is full.
5L11return=240 room=0Maximum value 240.00. Greedy is optimal here because fractions of any item are allowed.
21.04.3

Minimum platforms

Trains have arrival and departure times; how many platforms are needed so no train waits? Sort arrivals and departures **separately** and sweep with two pointers: if the next arrival is before the next departure, a platform is needed (count++), otherwise a train has left (count--). The maximum count is the answer. This is the event sweep, developed fully in Chapter 22, applied to trains.

The tie rule matters. In the half-open model a train leaving at time 3 frees the platform for one arriving at 3, so the “Back-to-back: half-open needs 1” preset needs 1 platform; if the station needs a gap between trains (closed intervals) the same data needs 2. The animation lets you switch the option and watch the answer change.

Java
1static int minPlatforms(int[] arrival, int[] departure) {
2 int[] arr = arrival.clone(), dep = departure.clone();
3 Arrays.sort(arr);
4 Arrays.sort(dep);
5 int platforms = 0, best = 0, j = 0;
6 for (int i = 0; i < arr.length; i++) {
7 while (dep[j] <= arr[i]) { j++; platforms--; } // half-open: leaving at t frees the platform for an arrival at t
8 platforms++;
9 best = Math.max(best, platforms);
10 }
11 return best;
12}

Best

O(n log n)

Average

O(n log n)

Worst

O(n log n)

Space

O(n) for the sorted copies

Start/end events: active count and maximum concurrency

Interactive · edit the input

Your input

Try

intervals

[0, 30]
[5, 10]
[15, 20]
[10, 15]
0123456789101112131415161718192021222324252627282930

events in sweep order

+0
+5
−10
+10
−15
+15
−20
−30

active count after each event

none
Setup

Turn every interval into two events, +1 at its start and −1 at its end, sort them, and sweep once keeping a running count. At equal coordinates ends come first, because [s, e) does not contain e: back-to-back intervals do not overlap.

Step 1 / 10
Java
1static int maxConcurrent(int[][] iv, boolean closed) {
2 List<int[]> events = new ArrayList<>(); // {coordinate, +1 start / -1 end}
3 for (int[] x : iv) { events.add(new int[]{x[0], +1}); events.add(new int[]{x[1], -1}); }
4 events.sort((a, b) -> a[0] != b[0] ? Integer.compare(a[0], b[0])
5 : closed ? Integer.compare(b[1], a[1]) : Integer.compare(a[1], b[1])); // tie: closed -> start first; half-open -> end first
6 int active = 0, best = 0;
7 for (int[] e : events) {
8 active += e[1]; // a start opens one, an end closes one
9 best = Math.max(best, active);
10 }
11 return best;
12}

Live state

intervals

4

events

8

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L2intervals=4 events=8Turn every interval into two events, +1 at its start and −1 at its end, sort them, and sweep once keeping a running count. At equal coordinates ends come first, because [s, e) does not contain e: back-to-back intervals do not overlap.
2L8x=0 event=start active=1 best=1At 0: an interval starts, so 1 is active. New maximum: 1.
3L8x=5 event=start active=2 best=2At 5: an interval starts, so 2 are active. New maximum: 2.
4L8x=10 event=end active=1 best=2At 10: an interval ends, so 1 is active.
5L8x=10 event=start active=2 best=2At 10: an interval starts, so 2 are active.
6L8x=15 event=end active=1 best=2At 15: an interval ends, so 1 is active.
7L8x=15 event=start active=2 best=2At 15: an interval starts, so 2 are active.
8L8x=20 event=end active=1 best=2At 20: an interval ends, so 1 is active.
9L8x=30 event=end active=0 best=2At 30: an interval ends, so 0 are active.
10L11return=2Maximum concurrency 2: that many rooms (or platforms, or CPUs) are needed. Cost O(n log n) for the sort, O(n) for the sweep.
21.04.4

Gas station

Stations sit on a circle; station i has gas[i] fuel and the leg to the next station costs cost[i]. Find the station to start from to complete the circuit, or report that none exists. Two observations give an O(n) algorithm. If total gas is less than total cost, no start works. Otherwise, a start exists, and it is found by scanning: keep the running tank; whenever it goes negative at station i, no station between the current start and i can be the answer, so restart from i + 1.

The first preset (gas 1 2 3 4 5, cost 3 4 5 1 2) restarts twice and ends with start station 3. “Impossible” has total net fuel -1, so the algorithm stops at once.

Java
1static int canCompleteCircuit(int[] gas, int[] cost) {
2 int total = 0, tank = 0, start = 0;
3 for (int i = 0; i < gas.length; i++) {
4 int net = gas[i] - cost[i];
5 total += net;
6 tank += net;
7 if (tank < 0) { start = i + 1; tank = 0; } // stations start..i cannot be the start
8 }
9 return total >= 0 ? start : -1;
10}

Best

O(n)

Average

O(n)

Worst

O(n)

Space

O(1)

Gas station: the starting stop of a circular trip

Interactive · edit the input

Your input

Try

gas

1s0
2s1
3s2
4s3
5s4

cost to next

3
4
5
1
2

tank after stop

0start s0
Setup

Drive around once, tracking the tank from the current candidate start. If the tank ever goes negative, no station between the start and here can be a valid start either, so the next stop becomes the new candidate.

Step 1 / 10
Java
1static int startStation(int[] gas, int[] cost) {
2 int total = 0, tank = 0, start = 0;
3 for (int i = 0; i < gas.length; i++) {
4 int gain = gas[i] - cost[i]; // net fuel at this stop
5 total += gain; tank += gain;
6 if (tank < 0) { start = i + 1; tank = 0; } // cannot reach i + 1 from the current start: restart after i
7 }
8 return total >= 0 ? start : -1; // enough fuel overall means the last candidate works
9}

Live state

total

0

tank

0

start

0

No condition evaluated on this step
Execution trace (dry run table)Show 10 steps
StepLineStateWhat happens
1L2total=0 tank=0 start=0Drive around once, tracking the tank from the current candidate start. If the tank ever goes negative, no station between the start and here can be a valid start either, so the next stop becomes the new candidate.
2L5stop=0 gain=-2 tank=-2 total=-2 start=0Stop 0: +1 gas, -3 to reach the next stop: net -2. Tank -2, total -2.
3L6stop=0 start=1 tank=0The tank is negative: the trip from the old start cannot pass stop 0. Restart at stop 1 with an empty tank.
4L5stop=1 gain=-2 tank=-2 total=-4 start=1Stop 1: +2 gas, -4 to reach the next stop: net -2. Tank -2, total -4.
5L6stop=1 start=2 tank=0The tank is negative: the trip from the old start cannot pass stop 1. Restart at stop 2 with an empty tank.
6L5stop=2 gain=-2 tank=-2 total=-6 start=2Stop 2: +3 gas, -5 to reach the next stop: net -2. Tank -2, total -6.
7L6stop=2 start=3 tank=0The tank is negative: the trip from the old start cannot pass stop 2. Restart at stop 3 with an empty tank.
8L5stop=3 gain=3 tank=3 total=-3 start=3Stop 3: +4 gas, -1 to reach the next stop: net +3. Tank 3, total -3.
9L5stop=4 gain=3 tank=6 total=0 start=3Stop 4: +5 gas, -2 to reach the next stop: net +3. Tank 6, total 0.
10L8total=0 return=3Total net fuel 0 is not negative, so starting at stop 3 completes the circuit.
21.04.5

Assign cookies

Each child has a greed factor (the smallest cookie size that satisfies them) and each cookie has a size. Each child gets at most one cookie. To satisfy as many children as possible, sort both lists and match the **smallest sufficient cookie** to the **least greedy unsatisfied child**: offer each cookie in increasing order, and give it to the current child only if it is big enough.

A larger cookie can always replace a smaller one in any solution, so using the smallest cookie that works loses nothing: the exchange argument again. The “Match smallest first” preset satisfies all 4 children.

Java
1static int findContentChildren(int[] greed, int[] cookies) {
2 int[] g = greed.clone(), s = cookies.clone();
3 Arrays.sort(g);
4 Arrays.sort(s);
5 int child = 0;
6 for (int cookie : s) {
7 if (child < g.length && cookie >= g[child]) child++; // this cookie satisfies the least greedy child left
8 }
9 return child;
10}

Best

O(n log n + m log m)

Average

O(n log n + m log m)

Worst

O(n log n + m log m)

Space

O(1) beyond the sorted copies

Assign cookies to the greediest children you can satisfy

Interactive · edit the input

Your input

Try

children (sorted greed)

1c0
2c1
3c2

cookies (sorted)

1
1
Setup

Sort both lists. Offer each cookie, smallest first, to the least greedy child who is still unsatisfied: a bigger cookie is never needed for a smaller appetite, so nothing is wasted.

Step 1 / 4
Java
1static int satisfied(int[] greed, int[] cookies) {
2 Arrays.sort(greed); Arrays.sort(cookies);
3 int child = 0;
4 for (int cookie : cookies) { // smallest cookie first
5 if (child < greed.length && cookie >= greed[child]) child++; // it satisfies the least greedy child left
6 } // otherwise this cookie is too small for everyone left: discard it
7 return child;
8}

Live state

children

3

cookies

2

No condition evaluated on this step
Execution trace (dry run table)Show 4 steps
StepLineStateWhat happens
1L2children=3 cookies=2Sort both lists. Offer each cookie, smallest first, to the least greedy child who is still unsatisfied: a bigger cookie is never needed for a smaller appetite, so nothing is wasted.
2L5cookie=1 next child=1 satisfied=0Cookie 1 satisfies the child with greed 1.
3L5cookie=1 next child=2 satisfied=1Cookie 1 is smaller than 2, the smallest greed left: it fits nobody, discard it.
4L7return=11 child satisfied.

Practice ladder

Now try it yourself

Easy

  • Assign Cookies
  • Lemonade Change

Medium

  • Gas Station
  • Maximum Units on a Truck
  • Meeting Rooms II

Advanced

  • Maximum Profit in Job Scheduling
  • IPO

Key takeaways

  • Job sequencing: sort by profit, place each job in the latest free slot not after its deadline.
  • Fractional knapsack: sort by value per weight; fractions make greedy optimal, whole items do not.
  • Minimum platforms is the maximum overlap, computed by sweeping sorted arrivals and departures.
  • Gas station: if total gas covers total cost, restart the start point whenever the running tank goes negative.
  • Match the smallest sufficient supply to the smallest demand with two pointers.