Skip to content
DSA greedy 12 min read

Greedy Algorithm Exercises: Answer Sheet & Worked Solutions

This is the answer sheet for the greedy exercises. Each solution gives the key idea, the optimal complexity, the code in all four languages (use the switcher), and a common pitfall. Try the problems first — and for each, make sure you can say why the greedy choice is provably safe, not just that it works.

Q1. Assign Cookies

Idea: Sort both children (greed) and cookies (size) ascending. Walk both with two pointers, giving the smallest cookie that satisfies the current child to the least-greedy unserved child. Spending the smallest sufficient cookie never wastes a bigger one.

Complexity: O(n log n) time (the sorts), O(1) extra space.

#include <vector>
#include <algorithm>

int findContentChildren(std::vector<int> g, std::vector<int> s) {
    std::sort(g.begin(), g.end());
    std::sort(s.begin(), s.end());
    int child = 0, cookie = 0;
    while (child < (int)g.size() && cookie < (int)s.size()) {
        if (s[cookie] >= g[child]) child++; // this cookie satisfies the child
        cookie++;                           // either way, this cookie is used up
    }
    return child;
}

Pitfall: Don’t always advance the child pointer. Advance it only when a cookie satisfies it; the cookie pointer advances every iteration because an undersized cookie can’t satisfy anyone larger either.

Q2. Fractional Knapsack

Idea: Sort items by value-to-weight ratio descending. Greedily take whole items while they fit; for the first item that doesn’t fit, take the fraction that fills the remaining capacity. Because fractions are allowed, the densest value always belongs in the bag — this is the greedy-choice property that fails for 0/1 knapsack.

Complexity: O(n log n) time, O(1) extra space (besides the sort).

#include <vector>
#include <algorithm>

// items: {value, weight}. Returns max value for capacity W.
double fractionalKnapsack(std::vector<std::pair<double,double>> items, double W) {
    std::sort(items.begin(), items.end(), [](auto& a, auto& b){
        return a.first / a.second > b.first / b.second; // ratio descending
    });
    double total = 0;
    for (auto& [value, weight] : items) {
        if (W <= 0) break;
        double take = std::min(weight, W);
        total += value * (take / weight); // full item or a fraction
        W -= take;
    }
    return total;
}

Pitfall: This greedy is correct only because fractions are allowed. For 0/1 knapsack (all-or-nothing items) the ratio rule fails — use dynamic programming.

Q3. Jump Game

Idea: Track the farthest index reachable so far. Sweep left to right; if you ever stand on an index beyond that reach, you’re stuck. Otherwise extend the reach by i + nums[i]. You can reach the end iff the reach ever covers the last index.

Complexity: O(n) time, O(1) space.

#include <vector>
bool canJump(const std::vector<int>& nums) {
    int reach = 0;
    for (int i = 0; i < (int)nums.size(); i++) {
        if (i > reach) return false;          // this index is unreachable
        reach = std::max(reach, i + nums[i]); // extend the frontier
    }
    return true;
}

Pitfall: Once i > reach you must stop — continuing reads indices you can never actually stand on and can produce a false positive.

Q4. Gas Station

Idea: If the total gas is less than total cost, no start works — return -1. Otherwise a valid start exists and is unique-ish: track a running tank from the current candidate start; the moment it goes negative at station i, no start in [start..i] can work, so the next candidate is i + 1.

Complexity: O(n) time, O(1) space.

#include <vector>
int canCompleteCircuit(const std::vector<int>& gas, const std::vector<int>& cost) {
    int total = 0, tank = 0, start = 0;
    for (int i = 0; i < (int)gas.size(); i++) {
        int diff = gas[i] - cost[i];
        total += diff;
        tank  += diff;
        if (tank < 0) { start = i + 1; tank = 0; } // restart after i
    }
    return total >= 0 ? start : -1;
}

Pitfall: The local tank resets to 0 at each restart, but total must accumulate across the whole array — it’s the global feasibility check. Mixing up the two breaks the answer.

Q5. Merge Intervals

Idea: Sort intervals by start. Walk through; if the current interval starts at or before the end of the last merged one, extend that end; otherwise start a new interval. Sorting guarantees any overlap is with the most recent block.

Complexity: O(n log n) time, O(n) space for the output.

#include <vector>
#include <algorithm>

std::vector<std::vector<int>> merge(std::vector<std::vector<int>> intervals) {
    std::sort(intervals.begin(), intervals.end());
    std::vector<std::vector<int>> out;
    for (auto& iv : intervals) {
        if (!out.empty() && iv[0] <= out.back()[1])
            out.back()[1] = std::max(out.back()[1], iv[1]); // overlap: extend
        else
            out.push_back(iv);                              // disjoint: new block
    }
    return out;
}

Pitfall: When extending, take max(last_end, current_end) — a later interval may be fully contained in the previous one, so blindly assigning its end can shrink the merged block.

Q6. Non-overlapping Intervals

Idea: This is activity selection flipped: the maximum number of non-overlapping intervals you can keep is found by sorting on end time and greedily keeping each interval whose start is at or after the last kept end. The minimum to remove is n − kept.

Complexity: O(n log n) time, O(1) extra space.

#include <vector>
#include <algorithm>

int eraseOverlapIntervals(std::vector<std::vector<int>> intervals) {
    std::sort(intervals.begin(), intervals.end(),
              [](auto& a, auto& b){ return a[1] < b[1]; }); // by end
    int kept = 0, lastEnd = INT_MIN;
    for (auto& iv : intervals)
        if (iv[0] >= lastEnd) { kept++; lastEnd = iv[1]; }
    return (int)intervals.size() - kept;
}

Pitfall: Sort by end, not start. Sorting by start and greedily keeping leads you to keep a long early interval that blocks many shorter ones — the classic wrong greedy for interval scheduling.

Q7. Minimum Number of Platforms

Idea: A platform is needed for every train present at the same time, so the answer is the maximum number of overlapping intervals. Sort arrivals and departures separately, then sweep with two pointers: each arrival before the next departure needs a new platform; each departure frees one. Track the running and maximum count.

Complexity: O(n log n) time, O(1) extra space (besides the sorts).

#include <vector>
#include <algorithm>

int minPlatforms(std::vector<int> arr, std::vector<int> dep) {
    std::sort(arr.begin(), arr.end());
    std::sort(dep.begin(), dep.end());
    int i = 0, j = 0, cur = 0, best = 0, n = arr.size();
    while (i < n) {
        if (arr[i] <= dep[j]) { cur++; i++; best = std::max(best, cur); }
        else { cur--; j++; } // a train has departed, free a platform
    }
    return best;
}

Pitfall: Use arr[i] <= dep[j] (not <) if a train arriving exactly when another departs still needs its own platform — match the comparison to the problem’s tie rule. Also loop only until arrivals are exhausted; remaining departures can’t raise the peak.


That’s the full answer sheet. Back to the greedy exercises, or continue to dynamic programming for the problems where greedy isn’t enough.

Last updated June 25, 2026
Was this helpful?