Lesson 30 of 50 intermediate

Greedy Algorithms — Always Pick the Best

Make the best choice at every step and hope for the best

Open interactive version (quiz + challenge)

Real-world analogy

Imagine you are at an all-you-can-eat buffet with a small plate. You want to eat the most delicious food possible. The greedy strategy? At each trip, pick the most delicious dish available right now. You do not plan ahead for future trips — you just grab the best thing you see each time. Sometimes this gives the overall best result, sometimes it does not. Knowing when it works is the key to greedy algorithms!

What is it?

A greedy algorithm is a problem-solving approach that makes the best possible choice at each step without reconsidering previous choices. It is like always picking the best option available right now. Greedy algorithms are fast and simple, but they only work for certain types of problems where local optimal choices lead to a global optimum.

Real-world relevance

Greedy algorithms power many real-world systems: GPS navigation (always take the shortest next road), Huffman coding in file compression (assign shortest codes to most frequent symbols), job scheduling in operating systems (shortest job first), and even how we naturally make decisions (take the closest parking spot).

Key points

Code example

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    // Problem 1: Coin Change (Greedy)
    int amount;
    cin >> amount;
    int coins[] = {25, 10, 5, 1};
    int totalCoins = 0;

    cout << "Making change for " << amount << " cents:\n";
    for (int i = 0; i < 4; i++) {
        int count = amount / coins[i];
        if (count > 0) {
            cout << count << " x " << coins[i] << " cents\n";
            totalCoins += count;
            amount %= coins[i];
        }
    }
    cout << "Total coins: " << totalCoins << "\n\n";

    // Problem 2: Activity Selection
    int n;
    cin >> n;
    vector<pair<int,int>> activities(n); // {end, start}
    for (int i = 0; i < n; i++) {
        int s, e;
        cin >> s >> e;
        activities[i] = {e, s}; // Store end first for sorting
    }
    sort(activities.begin(), activities.end()); // Sort by end time

    int count = 1;
    int lastEnd = activities[0].first;
    cout << "Selected: [" << activities[0].second << "," << activities[0].first << "]";

    for (int i = 1; i < n; i++) {
        if (activities[i].second >= lastEnd) { // Start >= last end
            count++;
            lastEnd = activities[i].first;
            cout << " [" << activities[i].second << "," << activities[i].first << "]";
        }
    }
    cout << "\nMax activities: " << count << "\n";

    return 0;
}

Line-by-line walkthrough

  1. 1. int count = amount / coins[i]; — integer division tells us how many of this coin denomination we can use. For 36 / 25 = 1, we use one 25-cent coin.
  2. 2. amount %= coins[i]; — the modulo gives us the remaining amount after using those coins. 36 % 25 = 11, so we still need 11 cents.
  3. 3. activities[i] = {e, s}; — we store end time first in the pair. Since sort() orders pairs by first element, this automatically sorts by end time.
  4. 4. sort(activities.begin(), activities.end()); — sorting by end time is the key greedy insight. The activity that ends earliest gives us the most room for future activities.
  5. 5. if (activities[i].second >= lastEnd) — we check if this activity starts at or after the last selected activity ends. If so, there is no overlap and we can include it.
  6. 6. lastEnd = activities[i].first; — update our tracking variable to the end time of the newly selected activity. Future activities must start after this time.

Spot the bug

#include <bits/stdc++.h>
using namespace std;

int main() {
    int n;
    cin >> n;
    vector<pair<int,int>> acts(n);
    for (int i = 0; i < n; i++) {
        cin >> acts[i].first >> acts[i].second; // start, end
    }
    sort(acts.begin(), acts.end()); // Sort by start time

    int count = 1;
    int lastEnd = acts[0].second;
    for (int i = 1; i < n; i++) {
        if (acts[i].first >= lastEnd) {
            count++;
            lastEnd = acts[i].second;
        }
    }
    cout << count << "\n";
    return 0;
}
Need a hint?
The greedy activity selection requires sorting by END time, not start time. What happens if you sort by start time instead?
Show answer
The bug is sorting by start time instead of end time. With start-time sorting, you might pick an activity that starts early but ends very late, blocking many short activities. For example, activities (1,100), (2,3), (4,5), (6,7): sorting by start picks (1,100) first and only gets 1 activity, but sorting by end gives (2,3), (4,5), (6,7) = 3 activities. Fix: store as {end, start} and sort, or use a custom comparator.

Explain like I'm 5

Imagine you are picking apples from trees along a path. At each tree, you pick the biggest apple you can reach. You do not think about trees ahead — you just grab the best one NOW and move on. That is greedy! Sometimes it gives you the most apples overall (like when all trees have similar sizes). But sometimes a tree with small apples now leads to a tree with HUGE apples later, and greedy misses it. The trick is knowing when 'always pick the best now' gives the best result overall.

Fun fact

The activity selection problem was one of the first problems proven to have an optimal greedy solution, studied by computer scientists in the 1970s. The greedy approach is not just theory — airline companies use variants of it to schedule flights and maximize the number of flights per gate. FedEx and UPS use greedy-based algorithms to optimize their delivery routes every single day!

Hands-on challenge

Implement the activity selection problem. Read n activities with start and end times, and find the maximum number of non-overlapping activities. Test with: 6 activities: (1,3), (2,5), (3,6), (5,7), (6,8), (8,10). The greedy solution should select 4 activities.

More resources

Open interactive version (quiz + challenge) ← Back to course: CP Zero to Hero