Greedy Algorithm | "Best Choice Every Step" Complete Guide

Greedy Algorithm | "Best Choice Every Step" Complete Guide

이 글의 핵심

Greedy algorithms make the locally optimal choice at each step. Learn when to use greedy, how to prove correctness, and master common greedy patterns.

Introduction

Greedy algorithms seem simple to implement, but without proving correctness, they can produce wrong answers. This guide helps you master typical greedy patterns and recognize when to switch to DP or other strategies.

”Does Best Choice Every Step Lead to Optimal Solution?”

Greedy algorithms make the best choice at each step. They don’t always guarantee optimal solutions, but when provably correct, they are very efficient.


1. What is Greedy Algorithm?

Concept

Greedy algorithms select only the local optimum at each step. Below is a coin change example that uses the largest denomination first. However, if coin denominations are arbitrary, greedy may not guarantee minimum count, so verify that greedy conditions (exchange property, sorting conditions, etc.) hold for the problem.

# Example: Coin change (500, 100, 50, 10 won)
# Make change for 1260 won

coins = [500, 100, 50, 10]
change = 1260
count = 0

for coin in coins:
    count += change // coin  # Use as many as possible
    change %= coin

print(count)  # 6 (500*2 + 100*2 + 50*1 + 10*1)

Greedy vs DP

FeatureGreedyDP
SelectionCurrent bestAll cases
Time ComplexityO(n log n)O(n²) ~ O(2ⁿ)
Optimal GuaranteeX (proof needed)O
ImplementationSimpleComplex

2. Representative Problems

Problem 1: Meeting Rooms

def max_meetings(meetings):
    """
    Schedule maximum number of meetings
    [(start, end), ...]
    """
    # Sort by end time
    meetings.sort(key=lambda x: x[1])
    
    count = 0
    last_end = 0
    
    for start, end in meetings:
        if start >= last_end:
            count += 1
            last_end = end
    
    return count

# Test
meetings = [(1, 4), (3, 5), (0, 6), (5, 7), (3, 8), (5, 9)]
print(max_meetings(meetings))  # 4
# Cannot select (1,4), (5,7), (5,9) → Select (1,4), (5,7)

Why Sort by End Time?

Sort by start time: ❌
[(0,6), (1,4), (3,5), (5,7)] → Select (0,6) only (1 meeting)

Sort by end time: ✅
[(1,4), (3,5), (0,6), (5,7)] → Select (1,4), (5,7), (5,9) (3 meetings)

Key: Early end time leaves more room for next meetings

Problem 2: Coin Change

def min_coins(coins, amount):
    """
    Minimum number of coins
    """
    coins.sort(reverse=True)
    count = 0
    
    for coin in coins:
        if amount >= coin:
            count += amount // coin
            amount %= coin
    
    return count if amount == 0 else -1

# Test
coins = [500, 100, 50, 10]
print(min_coins(coins, 1260))  # 6

# ❌ Greedy fails
coins = [10, 7, 1]
amount = 14
# Greedy: 10 + 1 + 1 + 1 + 1 = 5 coins
# Optimal: 7 + 7 = 2 coins (need DP!)

Problem 3: Create Largest Number

def make_largest_number(number, k):
    """
    Remove k digits to create largest number
    "1924", k=2 → "94"
    """
    stack = []
    removed = 0
    
    for digit in number:
        # Remove stack top if smaller than current
        while stack and removed < k and stack[-1] < digit:
            stack.pop()
            removed += 1
        stack.append(digit)
    
    # If haven't removed k digits, remove from end
    return ''.join(stack[:len(stack) - (k - removed)])

# Test
print(make_largest_number("1924", 2))  # "94"
print(make_largest_number("1231234", 3))  # "3234"

Step-by-Step Trace:

"1924", k=2

Step 1: digit='1', stack=['1']
Step 2: digit='9', stack=['9'] (remove '1', removed=1)
Step 3: digit='2', stack=['9','2']
Step 4: digit='4', stack=['9','4'] (remove '2', removed=2)

Result: "94"

3. Greedy Proof

Proof Methods

1) Exchange Argument

"Swapping greedy choice with another choice does not improve solution"

2) Proof by Contradiction

"Assume greedy is not optimal → Derive contradiction"

Example: Meeting Rooms Proof

"Selecting meeting with earliest end time is optimal"

Proof:
- Selecting another meeting means later end time
- Later end time reduces options for next meetings
- Therefore, earliest end time is optimal

When Greedy Fails

# Example: Coin change with arbitrary denominations
coins = [10, 7, 1]
amount = 14

# Greedy approach
def greedy_coins(coins, amount):
    coins.sort(reverse=True)
    count = 0
    for coin in coins:
        count += amount // coin
        amount %= coin
    return count

print(greedy_coins(coins, 14))  # 5 (10+1+1+1+1)

# Optimal solution (DP needed)
# 7 + 7 = 2 coins

# Lesson: Greedy fails when larger denomination
# prevents using optimal combination of smaller ones

4. Practical Problems

Problem 1: Gym Clothes (Programmers)

def solution(n, lost, reserve):
    """
    Lending gym clothes
    """
    # Exclude students who have reserve but also lost
    lost_set = set(lost) - set(reserve)
    reserve_set = set(reserve) - set(lost)
    
    for r in sorted(reserve_set):
        # Lend to lower number first
        if r - 1 in lost_set:
            lost_set.remove(r - 1)
        elif r + 1 in lost_set:
            lost_set.remove(r + 1)
    
    return n - len(lost_set)

# Test
print(solution(5, [2, 4], [1, 3, 5]))  # 5

Problem 2: Joystick (Programmers)

def solution(name):
    """
    Minimum joystick moves
    """
    answer = 0
    min_move = len(name) - 1  # Move right only
    
    for i, char in enumerate(name):
        # Up/down movement (A → char)
        answer += min(ord(char) - ord('A'), ord('Z') - ord(char) + 1)
        
        # Optimize left/right movement
        next_idx = i + 1
        while next_idx < len(name) and name[next_idx] == 'A':
            next_idx += 1
        
        # Right → Left vs Left → Right
        min_move = min(
            min_move,
            i + i + len(name) - next_idx,  # Right → Left
            i + len(name) - next_idx + len(name) - next_idx  # Left → Right
        )
    
    return answer + min_move

# Test
print(solution("JEROEN"))  # 56

5. Common Greedy Patterns

Pattern 1: Sorting + Selection

# Most greedy problems start with sorting

# 1. Sort by end time (meeting rooms)
meetings.sort(key=lambda x: x[1])

# 2. Sort descending (coin change)
coins.sort(reverse=True)

# 3. Custom sort (largest number)
nums.sort(key=lambda x: x*10, reverse=True)

Pattern 2: Stack-Based Greedy

def remove_k_digits(num, k):
    """
    Remove k digits to create smallest number
    """
    stack = []
    removed = 0
    
    for digit in num:
        # Remove if stack top is larger
        while stack and removed < k and stack[-1] > digit:
            stack.pop()
            removed += 1
        stack.append(digit)
    
    # Remove from end if needed
    result = ''.join(stack[:len(stack) - (k - removed)])
    
    # Remove leading zeros
    return result.lstrip('0') or '0'

# Test
print(remove_k_digits("1432219", 3))  # "1219"

Pattern 3: Two Pointers + Greedy

def assign_cookies(greed, cookies):
    """
    Assign cookies to children
    greed[i] = minimum cookie size for child i
    """
    greed.sort()
    cookies.sort()
    
    child = 0
    cookie = 0
    
    while child < len(greed) and cookie < len(cookies):
        if cookies[cookie] >= greed[child]:
            child += 1
        cookie += 1
    
    return child

# Test
greed = [1, 2, 3]
cookies = [1, 1]
print(assign_cookies(greed, cookies))  # 1

Practical Tips

Identifying Greedy Problems

# ✅ Greedy signals
# 1. "Max/min" + sorting hint
# 2. Tight time limit (need O(n log n))
# 3. Hard to find counterexample

# ❌ Greedy not possible
# 1. Counterexample exists
# 2. Need to consider "all cases"
# 3. Previous choices affect future (→ DP)

Debugging Strategy

# 1. Test with small examples
# - Manually verify optimal solution
# - Compare with greedy result

# 2. Look for counterexamples
# - Try edge cases
# - Try reverse sorted input

# 3. Compare with DP
# - If greedy fails, try DP
# - If DP too slow, prove greedy

Summary

Key Points

  1. Greedy: Best choice every step, O(n log n)
  2. Proof Required: Check for counterexamples
  3. Sorting: Most problems require sorting first
  4. Compare with DP: Greedy is faster but not always optimal

Problem Identification

SignalGreedyDP
SelectionCurrent bestAll cases
TimeO(n log n)O(n²)
ProofRequiredNot required
ExampleMeeting roomsKnapsack

Baekjoon

LeetCode

  • LeetCode 455: Assign Cookies
  • LeetCode 435: Non-overlapping Intervals
  • LeetCode 402: Remove K Digits
  • LeetCode 621: Task Scheduler

Programmers



Keywords

Greedy Algorithm, Greedy, Optimization, Sorting, Meeting Rooms, Coin Change, Coding Interview, Algorithm, Local Optimum, Global Optimum