Greedy Algorithms
A greedy algorithm always makes the choice that looks best at the moment. This chapter develops that idea through fractional knapsack, Huffman coding, job sequencing with deadlines, and the optimal merge pattern.
10.1Introduction to Greedy Algorithms
Profit, reliability, trust, and efficiency.
Cost, risk, and latency.
A locally optimal choice can be committed to without preventing a globally optimal solution.
An optimal solution contains optimal solutions to its subproblems.
10.2Steps to Design a Greedy Algorithm
Decide the sorting rule, selection criterion, or priority.
Consider the input elements one by one in greedy order.
Add a choice only when it keeps the partial solution feasible.
GREEDY(A):
candidates = order_by_greedy_rule(A)
solution = []
for candidate in candidates:
if feasible(candidate, solution):
solution.append(candidate)
return solution
10.3Fractional Knapsack Problem
| Item | Item 1 | Item 2 | Item 3 |
|---|---|---|---|
| Profit | 25 | 24 | 15 |
| Weight | 18 | 15 | 10 |
| Profit / weight | ≈ 1.39 | 1.60 | 1.50 |
Item 1 first earns 25; adding 2/15 of Item 2 gives 28.2.
Item 3 + 10/15 of Item 2 gives 31.
Item 2 first earns 24; adding 5/10 of Item 3 gives the optimum 31.5.
Second knapsack example
| Item | Item 1 | Item 2 | Item 3 | Capacity |
|---|---|---|---|---|
| Profit | 60 | 100 | 120 | 50 |
| Weight | 10 | 20 | 30 |
def fractional_knapsack(items, capacity):
for item in items:
item.ratio = item.profit / item.weight
items.sort(key=lambda item: item.ratio, reverse=True)
total_profit = 0
for item in items:
if capacity == 0:
break
if item.weight <= capacity:
capacity -= item.weight
total_profit += item.profit
else:
total_profit += item.profit * (capacity / item.weight)
break
return total_profit
10.4Huffman Coding
1. Problem data
| Character | A | E | I | O | U |
|---|---|---|---|---|---|
| Frequency | 5 | 7 | 3 | 6 | 4 |
The five character frequencies add up to 25 characters.
For this comparison, each character is stored in one 8-bit byte, so the data requires 25 × 8 = 200 bits.
Can we still reduce the total bits?
2. Huffman solution approach
- Merge the two least frequent nodes into a new node whose frequency is their sum.
- Put the new node back and repeat until one root remains.
- Add 0 to every left edge.
- Add 1 to every right edge.
- Traverse from the root to each leaf to obtain its Huffman code.
3. Final answer — the lecture’s Huffman codes
| Character | Frequency | Code | Data bits |
|---|---|---|---|
| A | 5 | 10 | 10 |
| E | 7 | 00 | 14 |
| I | 3 | 010 | 9 |
| O | 6 | 11 | 12 |
| U | 4 | 011 | 12 |
| Total | 25 | 12 mapping bits | 57 |
4. Verify the storage saving
| Character | Frequency | ASCII | ASCII bits | Fixed 3-bit | Fixed bits | Huffman | Huffman bits |
|---|---|---|---|---|---|---|---|
| A | 5 | 01000001 | 40 | 000 | 15 | 10 | 10 |
| E | 7 | 01000101 | 56 | 001 | 21 | 00 | 14 |
| I | 3 | 01001001 | 24 | 010 | 9 | 010 | 9 |
| O | 6 | 01001111 | 48 | 011 | 18 | 11 | 12 |
| U | 4 | 01010101 | 32 | 100 | 12 | 011 | 12 |
| Total | 25 | — | 200 | — | 75 | — | 57 |
Five ASCII symbols use 40 bits and five 3-bit codes use 15 bits: mapping overhead = 55 bits.
Five ASCII symbols use 40 bits and the five variable codes use 12 bits: mapping overhead = 52 bits.
25 characters × 8 bits = 200 bits.
Five characters need ⌈log₂ 5⌉ = 3 bits each. Data 75 + symbols 40 + codes 15 = 130 bits.
Data 57 + symbols 40 + codes 12 = 109 bits.
10.5Building a Huffman Tree with a Priority Queue
- Create a binary-tree node for every character and frequency.
- Place the nodes in a priority queue—a min heap ordered by frequency.
- Extract the two minimum nodes, attach them to a new node, and insert their sum.
- Repeat until the queue contains one node: the root.
HUFFMAN(C):
n = |C|
Q = C
for i = 1 to n - 1:
allocate a new node z
x = EXTRACT_MIN(Q)
y = EXTRACT_MIN(Q)
z.left = x
z.right = y
z.freq = x.freq + y.freq
INSERT(Q, z)
return EXTRACT_MIN(Q) # the root is the only node left
E:7 and IU:7.
Either may be extracted first. Also, the left and right children of any
merged node may be swapped. These choices change the printed 0/1 patterns,
but not the code lengths, prefix property, or weighted total of
57 bits.
Classic six-character construction
The second tree from the slides starts with
f:5, e:9, c:12, b:13, d:16, a:45.
Encoding and decoding: FACE
Using the same six frequencies and tree shape above, relabel the symbols as A:5, B:9, C:12, D:13, E:16 and F:45. With left edges labelled 0 and right edges labelled 1, the mapping is:
| Symbol | Frequency | Code |
|---|---|---|
| A | 5 | 1100 |
| B | 9 | 1101 |
| C | 12 | 100 |
| D | 13 | 101 |
| E | 16 | 111 |
| F | 45 | 0 |
FACE → 0 | 1100 | 100 | 111 → 01100100111
Decompression
The decoder needs the mapping table (or the equivalent Huffman tree) and the compressed bitstream.
- Read bits from left to right, adding one bit at a time to the current code.
- When the code matches a symbol in the table, output that symbol and clear the current code.
- Repeat until the bitstream has been decoded.
| Bits read until a match | Output | Remaining bits |
|---|---|---|
0 | F | 1100100111 |
1 → 11 → 110 → 1100 | A | 100111 |
1 → 10 → 100 | C | 111 |
1 → 11 → 111 | E | None |
0 already means F, so no
other code starts with 0; 1, 11 and 110
are incomplete, so reading continues until 1100 identifies A.
Each boundary is unambiguous. The bars above are only reading aids;
01100100111 decodes to FACE without separators.
10.6Job Sequencing with Deadlines
Operating-system task scheduling, project management, and manufacturing processes.
If the highest deadline is k, at most k unit-time jobs can be scheduled.
Greedy solution
- Sort the jobs in decreasing order of profit.
- For each job, place it in the latest empty slot that is no later than its deadline.
Results from the slides
| Example | Schedule by time slot | Total profit |
|---|---|---|
| First feasible arrangement | J1, J4, J3 | 20 + 15 + 15 = 50 |
| Greedy lecture solution | J2, J3, J1 | 18 + 15 + 20 = 53 |
| Exercise solution | C, A, E | 27 + 100 + 15 = 142 |
10.7Optimal Merge Pattern
Before optimizing a sequence of file merges, recall how two sorted arrays are merged in linear time.
i = j = m = 0
while i < len(A) and j < len(B):
if A[i] <= B[j]:
C[m] = A[i]
i += 1
else:
C[m] = B[j]
j += 1
m += 1
# Append the remaining elements from A or B.
| File | A | B | C | D |
|---|---|---|---|---|
| Size | 30 | 20 | 10 | 5 |
Greedy solution
- Insert all file sizes into a min heap (priority queue).
- Extract the two smallest files.
- Merge them; the merge cost is the sum of their sizes.
- Insert the merged file back into the heap.
- Repeat until one file remains; add every merge cost to the total.
Cost comparison from the slides
50 + 60 + 65 = 175
50 + 15 + 65 = 130
15 + 35 + 65 = 115
Reveal the answer
With total file size n and exactly four subfiles, there are three merges. Since four is constant, the worst-case merging time is Θ(n) in terms of n.