DEV Community

Timevolt
Timevolt

Posted on

Greedy Like a Jedi: Mastering Activity Selection with the Force

The Quest Begins (The "Why")

I remember the first time I saw an interview question about “maximum number of non‑overlapping meetings.” My brain instantly went into brute‑force mode: what if I try every subset? I wrote a recursive backtracking solution that explored 2ⁿ possibilities, watched the test cases timeout, and felt like I was trying to defeat a Death Star with a blaster pistol. Frustrating, right?

The problem kept popping up in mock interviews, on LeetCode, and even in a casual chat with a friend who swore there was a “trick” that made it trivial. I was skeptical—how could picking a meeting at random possibly lead to the best answer? I spent a few nights staring at whiteboards, drawing intervals, and wondering if I was missing some hidden pattern. That’s when the quest for a truly greedy insight began.

The Revelation (The Insight)

Here’s the treasure: if you always pick the activity that finishes earliest, you can never hurt your chances of fitting in more activities later. It sounds almost too simple, but there’s a neat exchange argument that proves it.

Imagine you have an optimal solution OPT that doesn’t start with the earliest‑finishing activity A. Let B be the first activity in OPT. Since A finishes no later than B, we can replace B with A and still have a feasible schedule (because A ends earlier, it leaves at least as much room for the rest). The new schedule has the same size as OPT but now starts with the greedy choice. Repeating this argument shows there’s an optimal solution that begins with the greedy pick, and after removing that activity the same reasoning applies to the remainder.

In plain English: the earliest‑finishing activity is a safe move—it never blocks a better future. Once you make that safe move, the problem shrinks to the same type on the remaining intervals, so you can repeat the step. That’s why the greedy strategy yields an optimal solution.

It felt like when Neo dodges bullets in the Matrix—you realize the system has a simple rule that lets you see the whole battlefield instantly.

Wielding the Power (Code & Examples)

The Struggle: Brute Force

def max_activities_brute(starts, ends):
    n = len(starts)
    best = 0

    def dfs(idx, last_end, count):
        nonlocal best
        if idx == n:
            best = max(best, count)
            return
        # skip current
        dfs(idx + 1, last_end, count)
        # take current if it doesn't clash
        if starts[idx] >= last_end:
            dfs(idx + 1, ends[idx], count + 1+1, last_end, count)

    dfs(0, -float('inf'), 0)
    return best
Enter fullscreen mode Exit fullscreen mode

The above explores every subset (O(2ⁿ)) and times out on anything beyond ~20 intervals.

The Victory: Greedy

def max_activities_greedy(starts, ends):
    # 1️⃣ Pair and sort by finishing time (the Jedi move)
    activities = sorted(zip(starts, ends), key=lambda x: x[1])
    count = 0
    last_end = -float('inf')

    for s, e in activities:
        if s >= last_end:          # 2️⃣ Take if it doesn’t overlap
            count += 1
            last_end = e           # 3️⃣ Update the barrier
    return count
Enter fullscreen mode Exit fullscreen mode

Why it’s O(n log n): The sorting step dominates; the subsequent scan is linear. If the input is already sorted by end time, the algorithm is pure O(n).

Common trap #1 – Sorting by start time

If you sort by start time instead, you might pick a long activity that blocks many short ones that finish earlier. The greedy proof only works with the earliest finishing rule.

Common trap #2 – Forgetting to update last_end

Neglecting to move the barrier forward lets you count overlapping activities, inflating the answer incorrectly.

Interview Problem #1 – Classic Activity Selection

Given start and end times of *n meetings, return the maximum number of meetings that can be attended without overlap.*

Solution: Directly use max_activities_greedy.

Interview Problem #2 – Meeting Rooms (LeetCode #252)

Determine if a person can attend all meetings (i.e., no two overlap).

After sorting by start time, you only need to check neighboring pairs:

def can_attend_meetings(intervals):
    intervals.sort(key=lambda x: x[0])   # sort by start
    for i in range(1, len(intervals)):
        if intervals[i][0] < intervals[i-1][1]:   # overlap
            return False
    return True
Enter fullscreen mode Exit fullscreen mode

The same sorting idea appears; the inner check is O(n), so overall O(n log n).

Why This New Power Matters

Armed with this greedy insight, you can slash interview time from exponential to linearithmic, impress interviewers with a clean proof, and tackle real‑world scheduling problems—think CPU task sequencing, conference room booking, or even allocating ad slots.

More importantly, you’ve added a tool to your problem‑solving belt: look for a safe, locally optimal choice that leaves the subproblem unchanged in structure. Whenever you see a “pick one, then repeat” pattern, ask: does an exchange argument exist? If yes, you’ve likely found a greedy solution.

Your Turn

Grab a scrap of paper, draw a handful of intervals, and try the earliest‑finish rule by hand. Then code it up and test against random cases. Notice how the greedy answer always matches the brute‑force optimum for small n.

When you feel comfortable, throw in a twist—maybe each activity has a weight, or you need to minimize the number of resources instead of maximizing count. See where the greedy proof breaks and where you need to switch to DP or flow.

Challenge: Implement the greedy solution for “Maximum number of non‑overlapping intervals” in a language you don’t use daily (Rust, Go, or even Bash with sort and awk). Share your snippet in the comments—I’d love to see your Jedi code in action!

May the Force be with your intervals. 🚀

Top comments (0)