AdvancedDSA ยท Lesson 3

Greedy Algorithms

Solve optimization problems with greedy strategies: interval scheduling, minimum spanning trees, and classic greedy patterns

What is a Greedy Algorithm?

A greedy algorithm makes the locally optimal choice at each step, hoping to find a global optimum.

When greedy works: The problem has the greedy choice property โ€” a local optimal choice leads to a global optimal solution.

When it fails: When you need to consider future consequences (use DP instead).

Interval Scheduling

Activity Selection (Maximum Non-Overlapping Intervals)

def activity_selection(intervals):
    """Select max number of non-overlapping intervals
    Strategy: always pick the interval that ends earliest
    """
    # Sort by end time
    intervals.sort(key=lambda x: x[1])
    count = 0
    last_end = float('-inf')

    for start, end in intervals:
        if start >= last_end:
            count += 1
            last_end = end
    return count

intervals = [[1,2],[2,3],[3,4],[1,3]]
print(activity_selection(intervals))  # 3 (non-overlapping)

Merge Overlapping Intervals

def merge_intervals(intervals):
    intervals.sort(key=lambda x: x[0])
    merged = [intervals[0]]

    for start, end in intervals[1:]:
        if start <= merged[-1][1]:   # overlaps
            merged[-1][1] = max(merged[-1][1], end)
        else:
            merged.append([start, end])
    return merged

print(merge_intervals([[1,3],[2,6],[8,10],[15,18]]))
# [[1,6],[8,10],[15,18]]

Minimum Number of Platforms

def min_platforms(arrivals, departures):
    """Minimum platforms needed at a train station"""
    arrivals.sort()
    departures.sort()
    platforms = 1
    max_platforms = 1
    i, j = 1, 0

    while i < len(arrivals) and j < len(departures):
        if arrivals[i] <= departures[j]:
            platforms += 1
            i += 1
            max_platforms = max(max_platforms, platforms)
        else:
            platforms -= 1
            j += 1
    return max_platforms

Jump Game Variants

Can Reach End?

def can_jump(nums):
    """Can you reach the last index?"""
    max_reach = 0
    for i, jump in enumerate(nums):
        if i > max_reach:
            return False
        max_reach = max(max_reach, i + jump)
    return True

print(can_jump([2, 3, 1, 1, 4]))  # True
print(can_jump([3, 2, 1, 0, 4]))  # False

Minimum Jumps

def jump(nums):
    """Minimum jumps to reach the last index"""
    n = len(nums)
    jumps = 0
    current_end = 0
    farthest = 0

    for i in range(n - 1):
        farthest = max(farthest, i + nums[i])
        if i == current_end:
            jumps += 1
            current_end = farthest
    return jumps

print(jump([2, 3, 1, 1, 4]))   # 2
print(jump([2, 3, 0, 1, 4]))   # 2

Task Scheduling

Assign Cookies

def find_content_children(greed, size):
    """Give each child at most one cookie; maximize satisfied children"""
    greed.sort()
    size.sort()
    child, cookie = 0, 0
    while child < len(greed) and cookie < len(size):
        if size[cookie] >= greed[child]:
            child += 1
        cookie += 1
    return child

print(find_content_children([1, 2, 3], [1, 1]))     # 1
print(find_content_children([1, 2], [1, 2, 3]))      # 2

Gas Station

def can_complete_circuit(gas, cost):
    """Find starting station to complete circular route"""
    total = 0
    tank = 0
    start = 0

    for i in range(len(gas)):
        gain = gas[i] - cost[i]
        total += gain
        tank += gain
        if tank < 0:
            start = i + 1
            tank = 0

    return start if total >= 0 else -1

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

Huffman Coding

import heapq

def huffman_codes(freq):
    """Build optimal prefix-free encoding"""
    heap = [(f, i, ch) for i, (ch, f) in enumerate(freq.items())]
    heapq.heapify(heap)

    codes = {}
    counter = len(freq)

    while len(heap) > 1:
        f1, _, n1 = heapq.heappop(heap)
        f2, _, n2 = heapq.heappop(heap)

        merged = (f1 + f2, counter, {**{k: '0' + v for k, v in (n1 if isinstance(n1, dict) else {n1: ''}).items()},
                                     **{k: '1' + v for k, v in (n2 if isinstance(n2, dict) else {n2: ''}).items()}})
        heapq.heappush(heap, merged)
        counter += 1

    return heap[0][2] if heap else {}

freq = {'a': 5, 'b': 9, 'c': 12, 'd': 13, 'e': 16, 'f': 45}
codes = huffman_codes(freq)
print(codes)

Exercises

Exercise 1: Two City Scheduling

Send N people to two cities at minimum cost. Exactly N/2 to each.

Solution:

def two_city_sched_cost(costs):
    # Sort by difference: cost_A - cost_B
    # Send people with lowest (costA - costB) to city A
    costs.sort(key=lambda x: x[0] - x[1])
    n = len(costs) // 2
    return sum(costs[i][0] for i in range(n)) + sum(costs[i][1] for i in range(n, 2*n))

print(two_city_sched_cost([[10,20],[30,200],[400,50],[30,20]]))  # 110

Exercise 2: Partition Labels

Partition string so each letter appears in at most one part; maximize parts.

Solution:

def partition_labels(s):
    last = {c: i for i, c in enumerate(s)}
    result = []
    start = end = 0
    for i, c in enumerate(s):
        end = max(end, last[c])
        if i == end:
            result.append(end - start + 1)
            start = i + 1
    return result

print(partition_labels("ababcbacadefegdehijhklij"))
# [9, 7, 8]