Category 7

Greedy Algorithms

Master the greedy choice property where locally optimal decisions lead to globally optimal solutions. Learn when greedy works, when it fails, and classic patterns including activity selection, interval scheduling, and jump game variants.

12
Problems
3
Difficulty Levels
100%
Complete Solutions
6
Codility Problems
Theory

The Greedy Choice Property

A problem exhibits the greedy choice property when a sequence of locally optimal choices yields a globally optimal solution. Understanding when and why greedy works—and when it fails—is essential for algorithm design.

1. Greedy Choice Property

Definition: At each step, make the locally optimal choice (often the "best" by some metric: minimum, maximum, earliest, latest). If these choices combine to form a globally optimal solution, the problem is greedy-solvable.

When it works: Problems with optimal substructure where future choices don't invalidate past decisions. Example: Activity selection (pick earliest-ending activities greedily).

When it fails: Problems where greedy decisions create constraints that block better global solutions. Example: Longest increasing subsequence (greedy pick of largest elements fails).

💡
Greedy vs. Dynamic Programming: If greedy seems natural but fails on small test cases, try DP. If DP works but is slow, ask whether greedy observation (like "always pick smallest ending time") can reduce complexity.

2. Sorting as Preprocessing

Many greedy problems require sorting first. Sort by different keys depending on the problem:

3. Key Patterns

Interval Scheduling: Select maximum number of non-overlapping intervals. Greedy: sort by end time, pick earliest-ending intervals.

Jump Games: Can you reach the end? Track maximum reachable index at each position. Greedy: always jump to maximum reachable position.

Gap Filling: Schedule tasks with gaps between them. Greedy: sort by frequency/deadline, fill largest gaps with most-frequent tasks.

csharp
// Template: Activity Selection (Sort + Greedy)
public int ActivitySelection(int[] starts, int[] ends) {
    int n = starts.Length;
    var activities = new int[n];
    for (int i = 0; i < n; i++) activities[i] = i;

    // Sort by end time
    System.Array.Sort(activities, (a, b) => ends[a].CompareTo(ends[b]));

    int count = 0;
    int lastEnd = 0;
    foreach (int i in activities) {
        if (starts[i] >= lastEnd) {
            count++;
            lastEnd = ends[i];
        }
    }
    return count;
}
csharp
// Template: Jump Game (Track Maximum Reachable)
public bool CanReachEnd(int[] nums) {
    int maxReach = 0;
    for (int i = 0; i < nums.Length; i++) {
        if (i > maxReach) return false;
        maxReach = Math.Max(maxReach, i + nums[i]);
    }
    return true;
}
Problems

12 Greedy Algorithm Problems

1. MaxProfit — Best Time to Buy and Sell Stock
Given an array of prices, find the maximum profit from one buy-sell transaction (buy before sell, single transaction only).
Codility Difficulty ★★
1
Time
O(n)
Space
O(1)
Source
Codility, LeetCode

Problem Statement

You are given an array prices where prices[i] is the price of a stock on day i. You may buy and sell the stock, but only once. Return the maximum profit. If no profit is possible, return 0.

Approach

1
Greedy Insight: Track the minimum price seen so far. At each price, compute profit if sold at that price. Greedily update maximum profit.
2
Why it works: To maximize profit, buy at the lowest point before any selling point. Since we process left-to-right, the minimum so far is the best buying point for each sale position.
3
Implementation: Single pass, track min price and max profit.
csharp
public int MaxProfit(int[] prices) {
    if (prices == null || prices.Length < 2) return 0;

    int minPrice = prices[0];
    int maxProfit = 0;

    for (int i = 1; i < prices.Length; i++) {
        int profit = prices[i] - minPrice;
        maxProfit = Math.Max(maxProfit, profit);
        minPrice = Math.Min(minPrice, prices[i]);
    }

    return maxProfit;
}

Trace Example

prices = [7, 1, 5, 3, 6, 4]

  • i=1: min=1, profit=1-7=-6, maxProfit=0
  • i=2: min=1, profit=5-1=4, maxProfit=4
  • i=3: min=1, profit=3-1=2, maxProfit=4
  • i=4: min=1, profit=6-1=5, maxProfit=5
  • i=5: min=1, profit=4-1=3, maxProfit=5

Output: 5 (buy at 1, sell at 6)

Edge Cases

  • Prices strictly decreasing: return 0
  • Single element: return 0
  • All same price: return 0
2. TieRopes — Minimum Ropes to Tie for Length K
Given rope lengths and a target length K, find the minimum number of ropes to tie together to reach at least K.
Codility Difficulty ★★
2
Time
O(n)
Space
O(1)
Source
Codility

Problem Statement

You have an array of rope lengths. You need to tie them together to create a rope of at least length K. When you tie two ropes together, the resulting length is the sum of both. Find the minimum number of ropes needed.

Approach

1
Greedy Insight: Sort ropes in descending order. Take the longest ropes first to reach K as quickly as possible.
2
Why it works: To minimize count, use largest values. Each additional rope adds the least overhead when it's the smallest one used.
3
Implementation: Sort descending, accumulate until sum ≥ K.
csharp
public int TieRopes(int[] lengths, int K) {
    if (lengths == null || lengths.Length == 0) return -1;

    System.Array.Sort(lengths, (a, b) => b.CompareTo(a)); // Descending

    int sum = 0;
    int count = 0;

    foreach (int length in lengths) {
        sum += length;
        count++;
        if (sum >= K) return count;
    }

    return -1; // Cannot reach K
}

Trace Example

lengths = [4, 3, 2, 6], K = 10

  • Sorted descending: [6, 4, 3, 2]
  • count=1, sum=6 (< 10)
  • count=2, sum=10 (>= 10) → return 2

Edge Cases

  • Sum of all ropes < K: return -1
  • K = 0: return 0 or 1 (clarify with problem)
  • Empty array: return -1
3. MaxNonoverlappingSegments — Greedy Interval Selection
Given intervals, find the maximum number of non-overlapping segments. Classic activity selection problem.
Codility Difficulty ★★
3
Time
O(n log n)
Space
O(n)
Source
Codility

Problem Statement

Given an array of intervals (each with start and end), find the maximum number of non-overlapping intervals you can select.

Approach

1
Greedy Insight: Sort intervals by end time. Greedily select intervals that end earliest and don't overlap with the last selected.
2
Why it works: By picking earliest-ending intervals, we leave maximum room for future selections.
3
Non-overlap check: Two intervals [a,b] and [c,d] don't overlap if b < c or d < a (assuming b < d). Check b ≤ c for "touching" non-overlap.
csharp
public int MaxNonoverlappingSegments(int[] A, int[] B) {
    int n = A.Length;
    var intervals = new int[n];
    for (int i = 0; i < n; i++) intervals[i] = i;

    // Sort by end time (B[i])
    System.Array.Sort(intervals, (i, j) => B[i].CompareTo(B[j]));

    int count = 0;
    int lastEnd = int.MinValue;

    foreach (int i in intervals) {
        if (A[i] > lastEnd) {
            count++;
            lastEnd = B[i];
        }
    }

    return count;
}

Trace Example

A = [1, 3, 7, 9, 9], B = [5, 6, 6, 9, 10]

Intervals: [1,5], [3,6], [7,6], [9,9], [9,10]

Sorted by end: [7,6], [1,5], [3,6], [9,9], [9,10]

  • Select [7,6], lastEnd=6, count=1
  • Check [1,5]: 1 ≤ 6, skip
  • Check [3,6]: 3 ≤ 6, skip
  • Check [9,9]: 9 > 6, select, lastEnd=9, count=2
  • Check [9,10]: 9 ≤ 9, skip

Output: 2

Edge Cases

  • All intervals overlap: return 1
  • All intervals non-overlapping: return n
  • Empty array: return 0
4. Jump Game — Can Reach End?
Given an array where each element is max jump length, determine if you can reach the last index starting from index 0.
LeetCode 55 Difficulty ★★
4
Time
O(n)
Space
O(1)
Source
LeetCode

Problem Statement

Given an array nums where nums[i] is the maximum jump length from index i, return true if you can reach the last index.

Approach

1
Greedy Insight: Track the maximum index reachable at each position. If current index > max reachable, you're stuck.
2
Why it works: You don't need to try all jump paths. Just track the farthest position reachable.
3
Implementation: Iterate left-to-right, update maxReach = max(maxReach, i + nums[i]).
csharp
public bool CanJump(int[] nums) {
    int maxReach = 0;

    for (int i = 0; i < nums.Length; i++) {
        if (i > maxReach) return false; // Can't reach this index
        maxReach = Math.Max(maxReach, i + nums[i]);
        if (maxReach >= nums.Length - 1) return true;
    }

    return false;
}

Trace Example

nums = [2, 3, 1, 1, 4] (last index = 4)

  • i=0: maxReach = max(0, 0+2) = 2
  • i=1: maxReach = max(2, 1+3) = 4 (≥ 4) → return true

nums = [3, 2, 1, 0, 4]

  • i=0: maxReach = 3
  • i=1: maxReach = max(3, 1+2) = 3
  • i=2: maxReach = max(3, 2+1) = 3
  • i=3: maxReach = max(3, 3+0) = 3 (stuck, can't reach index 4)
  • return false

Edge Cases

  • nums = [0]: return true (already at end)
  • nums = [0, 1]: return false (can't leave index 0)
  • Single large jump that overshoots: still return true
5. Jump Game II — Minimum Number of Jumps
Find the minimum number of jumps to reach the last index (guaranteed reachable).
LeetCode 45 Difficulty ★★
5
Time
O(n)
Space
O(1)
Source
LeetCode

Problem Statement

Given an array where each element is max jump length, return the minimum jumps to reach the last index. Guaranteed reachable from index 0.

Approach

1
Greedy Insight: Use BFS-like approach without explicit queue. Track current jump's max reach and next jump's max reach.
2
Why it works: We must reach the end. Greedily take all reachable indices from current jump and count each new "level" as one additional jump.
3
Implementation: Maintain currentEnd and nextEnd. When i reaches currentEnd, increment jumps and update currentEnd to nextEnd.
csharp
public int Jump(int[] nums) {
    if (nums.Length <= 1) return 0;

    int jumps = 0;
    int currentEnd = 0; // End of current jump's range
    int nextEnd = 0;   // Farthest we can reach from current range

    for (int i = 0; i < nums.Length - 1; i++) {
        nextEnd = Math.Max(nextEnd, i + nums[i]);

        if (i == currentEnd) {
            jumps++;
            currentEnd = nextEnd;
        }
    }

    return jumps;
}

Trace Example

nums = [2, 3, 1, 1, 4]

  • i=0: nextEnd=max(0, 0+2)=2. i==currentEnd, jumps=1, currentEnd=2
  • i=1: nextEnd=max(2, 1+3)=4. i!=currentEnd
  • i=2: nextEnd=max(4, 2+1)=4. i==currentEnd, jumps=2, currentEnd=4
  • Loop ends (i < 4)

Output: 2

Edge Cases

  • nums = [1, 1, 1, 1]: return 3
  • nums = [10, 1, 1, 1]: return 1 (can jump straight to end)
  • nums = [2, 3, 1, 1, 4]: return 2
6. Gas Station — Circular Route Feasibility
Given gas and cost arrays for a circular route, find the starting station to complete the loop (or -1 if impossible).
LeetCode 134 Difficulty ★★
6
Time
O(n)
Space
O(1)
Source
LeetCode

Problem Statement

You have a circular route with n stations. At each station i, you can get gas[i] gas and spend cost[i] gas to reach the next station. Find the starting station index such that you can travel the entire loop. If impossible, return -1.

Approach

1
Greedy Key: If total gas < total cost, return -1 (no solution exists).
2
Greedy Choice: If we can't reach station j from station i, then no earlier start can reach j via i. Start from j+1.
3
Why it works: Negative total at index i means starting from any earlier position fails even better. Only reset to i+1.
csharp
public int CanCompleteCircuit(int[] gas, int[] cost) {
    long totalGas = 0, totalCost = 0;
    foreach (int g in gas) totalGas += g;
    foreach (int c in cost) totalCost += c;

    if (totalGas < totalCost) return -1;

    int start = 0;
    long currentGas = 0;

    for (int i = 0; i < gas.Length; i++) {
        currentGas += gas[i] - cost[i];
        if (currentGas < 0) {
            start = i + 1;
            currentGas = 0;
        }
    }

    return start;
}

Trace Example

gas = [1, 2, 3, 4, 5], cost = [3, 4, 5, 1, 2]

  • totalGas = 15, totalCost = 15 (feasible)
  • i=0: currentGas = 1-3 = -2 (< 0), start=1, currentGas=0
  • i=1: currentGas = 2-4 = -2 (< 0), start=2, currentGas=0
  • i=2: currentGas = 3-5 = -2 (< 0), start=3, currentGas=0
  • i=3: currentGas = 4-1 = 3 (≥ 0)
  • i=4: currentGas = 3+5-2 = 6 (≥ 0)

Output: 3

Edge Cases

  • totalGas < totalCost: return -1
  • Single station: check if gas[0] ≥ cost[0]
  • Multiple valid starts: any one is acceptable
7. Stock II — Multiple Transactions
Find maximum profit with unlimited buy-sell transactions (must sell before buying again).
LeetCode 122 Difficulty ★★
7
Time
O(n)
Space
O(1)
Source
LeetCode

Problem Statement

Given prices for multiple days, find the max profit with unlimited transactions. You must sell before buying again.

Approach

1
Key Insight: Every upward slope is a profit opportunity. If price[i+1] > price[i], capture the gain.
2
Why it works: Breaking any increase into many small transactions gives the same total profit.
3
Implementation: Sum all positive differences (next - current).
csharp
public int MaxProfitMultiple(int[] prices) {
    int profit = 0;

    for (int i = 0; i < prices.Length - 1; i++) {
        int gain = prices[i + 1] - prices[i];
        if (gain > 0) {
            profit += gain;
        }
    }

    return profit;
}

Trace Example

prices = [7, 1, 5, 3, 6, 4]

  • 1 → 5: gain = 4
  • 5 → 3: loss (skip)
  • 3 → 6: gain = 3
  • 6 → 4: loss (skip)
  • Total profit = 4 + 3 = 7

Edge Cases

  • Prices strictly decreasing: return 0
  • Prices strictly increasing: sum all differences
  • Single element: return 0
8. Assign Cookies — Greedy Matching
Given children with greed factors and cookies with sizes, maximize satisfied children using greedy matching.
LeetCode 455 Difficulty ★
8
Time
O(n log n)
Space
O(1)
Source
LeetCode

Problem Statement

You have children with greed factors and cookies with sizes. A child is satisfied if cookie size ≥ greed factor. Maximize the number of satisfied children (each cookie to one child).

Approach

1
Greedy Insight: Sort both arrays. Assign smallest cookie to least greedy child, then move forward.
2
Why it works: Assigning a large cookie to a less-greedy child wastes it. Use small cookies for picky kids first.
3
Implementation: Two pointers, match greedily, advance both on success, advance cookies on failure.
csharp
public int FindContentChildren(int[] g, int[] s) {
    System.Array.Sort(g); // Children greed factors
    System.Array.Sort(s); // Cookie sizes

    int childIdx = 0, cookieIdx = 0;

    while (childIdx < g.Length && cookieIdx < s.Length) {
        if (s[cookieIdx] >= g[childIdx]) {
            childIdx++; // Child satisfied, move to next child
        }
        cookieIdx++; // Move to next cookie
    }

    return childIdx;
}

Trace Example

g = [1, 2, 3], s = [1, 1]

  • Both sorted: g=[1,2,3], s=[1,1]
  • cookieIdx=0: s[0]=1 ≥ g[0]=1, childIdx=1, cookieIdx=1
  • cookieIdx=1: s[1]=1 < g[1]=2, cookieIdx=2
  • cookieIdx=2 (out of bounds), stop

Output: 1

Edge Cases

  • Empty children or cookies: return 0
  • All cookies satisfy all children: return children.length
  • No cookies satisfy any child: return 0
9. Non-overlapping Intervals — Minimum Removals
Given intervals, find the minimum number to remove to make all remaining non-overlapping.
LeetCode 435 Difficulty ★★
9
Time
O(n log n)
Space
O(n)
Source
LeetCode

Problem Statement

Given a list of intervals, return the minimum number of intervals to remove so that the remaining intervals don't overlap.

Approach

1
Insight: Minimum removals = total intervals - maximum non-overlapping intervals.
2
Greedy: Sort by end time. Greedily select earliest-ending intervals (same as MaxNonoverlappingSegments).
3
Implementation: Count selected intervals, return total - count.
csharp
public int EraseOverlapIntervals(int[][] intervals) {
    if (intervals.Length <= 1) return 0;

    System.Array.Sort(intervals, (a, b) => a[1].CompareTo(b[1])); // Sort by end

    int count = 1;
    int lastEnd = intervals[0][1];

    for (int i = 1; i < intervals.Length; i++) {
        if (intervals[i][0] >= lastEnd) {
            count++;
            lastEnd = intervals[i][1];
        }
    }

    return intervals.Length - count;
}

Trace Example

intervals = [[1,2], [2,3], [3,4], [1,3]]

  • Sorted by end: [[1,2], [2,3], [1,3], [3,4]]
  • Select [1,2], lastEnd=2, count=1
  • [2,3]: start=2 ≥ 2, select, lastEnd=3, count=2
  • [1,3]: start=1 < 3, skip
  • [3,4]: start=3 ≥ 3, select, lastEnd=4, count=3

Output: 4 - 3 = 1 (remove 1 interval)

Edge Cases

  • Empty or single interval: return 0
  • All overlapping: remove all but one
  • All non-overlapping: return 0
10. Burst Balloons — Interval Piercing
Given balloon positions as intervals, find minimum arrows to burst all balloons.
LeetCode 452 Difficulty ★★
10
Time
O(n log n)
Space
O(n)
Source
LeetCode

Problem Statement

Balloons are represented by [xstart, xend]. An arrow shot at coordinate x bursts all balloons where xstart ≤ x ≤ xend. Find minimum arrows needed.

Approach

1
Insight: Similar to interval scheduling but pierce at the end of the first balloon, burst all overlapping.
2
Greedy: Sort by end position. Shoot at the end of the first balloon. Skip all overlapping balloons.
3
Difference: Two balloons overlap if xstart1 ≤ xend2 (not strict <). Use lastEnd as the arrow position.
csharp
public int FindMinArrowShots(int[][] points) {
    if (points.Length == 0) return 0;

    System.Array.Sort(points, (a, b) => {
        // Handle overflow: use long for comparison
        long diff = (long)a[1] - (long)b[1];
        if (diff < 0) return -1;
        if (diff > 0) return 1;
        return 0;
    });

    int arrows = 1;
    long lastEnd = points[0][1];

    for (int i = 1; i < points.Length; i++) {
        if (points[i][0] > lastEnd) {
            arrows++;
            lastEnd = points[i][1];
        }
    }

    return arrows;
}

Trace Example

points = [[10,16], [2,8], [1,6], [7,12]]

  • Sorted by end: [[1,6], [2,8], [7,12], [10,16]]
  • arrows=1, lastEnd=6
  • [2,8]: start=2 ≤ 6, skip
  • [7,12]: start=7 > 6, arrows=2, lastEnd=12
  • [10,16]: start=10 ≤ 12, skip

Output: 2

Edge Cases

  • Overflow: use long for comparisons
  • All balloons overlap at one point: arrows=1
  • Single balloon: arrows=1
11. Task Scheduler — Gap Filling Strategy
Given tasks with cooldown period, find minimum time to schedule all tasks.
LeetCode 621 Difficulty ★★
11
Time
O(n log n)
Space
O(1)
Source
LeetCode

Problem Statement

You have tasks represented as characters (same task type requires cooldown n between executions). Return the minimum time units to execute all tasks.

Approach

1
Greedy Insight: Execute most frequent tasks first. Fill gaps with other tasks or idle time.
2
Formula: If max frequency is maxFreq, and cooldown is n, then minimum time = (maxFreq - 1) * (n + 1) + (number of tasks with maxFreq).
3
Why it works: We need at least (maxFreq - 1) gaps, each of size (n + 1). After last instance, no gap needed.
csharp
public int LeastInterval(char[] tasks, int n) {
    // Count frequency of each task
    int[] freq = new int[26];
    int maxFreq = 0;

    foreach (char task in tasks) {
        freq[task - 'A']++;
        maxFreq = Math.Max(maxFreq, freq[task - 'A']);
    }

    // Count tasks with max frequency
    int countMaxFreq = 0;
    for (int i = 0; i < 26; i++) {
        if (freq[i] == maxFreq) countMaxFreq++;
    }

    // Formula: (maxFreq - 1) * (n + 1) + countMaxFreq
    int minTime = (maxFreq - 1) * (n + 1) + countMaxFreq;

    // At least tasks.Length time needed if many unique tasks
    return Math.Max(minTime, tasks.Length);
}

Trace Example

tasks = ['A', 'A', 'A', 'B', 'B', 'C'], n = 2

  • freq: A=3, B=2, C=1
  • maxFreq=3, countMaxFreq=1
  • minTime = (3-1) * (2+1) + 1 = 2*3 + 1 = 7
  • Execution: A _ _ A _ _ A B C or similar

Output: 8 (actual schedule may need gaps filled)

Edge Cases

  • All tasks same: return tasks.length
  • Many unique tasks, small n: return tasks.length (no idle time)
  • n=0: return tasks.length (no cooldown)
12. Partition Labels — Last Occurrence Greedy
Partition string so each letter appears in only one partition. Return partition sizes.
LeetCode 763 Difficulty ★★
12
Time
O(n)
Space
O(1)
Source
LeetCode

Problem Statement

Given a string, partition it such that each letter appears in at most one part. Return the sizes of these partitions.

Approach

1
Preprocessing: Record the last occurrence index of each character.
2
Greedy Partition: At each position, track the maximum last occurrence seen so far. When current index reaches this max, partition here.
3
Why it works: Can't partition before the last occurrence of any character in the partition.
csharp
public IList<int> PartitionLabels(string s) {
    // Record last occurrence of each character
    int[] lastOccur = new int[26];
    for (int i = 0; i < s.Length; i++) {
        lastOccur[s[i] - 'a'] = i;
    }

    var result = new List<int>();
    int start = 0, maxEnd = 0;

    for (int i = 0; i < s.Length; i++) {
        maxEnd = Math.Max(maxEnd, lastOccur[s[i] - 'a']);

        if (i == maxEnd) {
            // Partition here
            result.Add(i - start + 1);
            start = i + 1;
        }
    }

    return result;
}

Trace Example

s = "ababcbacaddefegdehijhijk"

  • lastOccur: a=8, b=5, c=7, d=13, e=15, f=11, g=12, h=19, i=22, j=23, k=24
  • i=0: maxEnd=8
  • i=1-7: update maxEnd as needed
  • i=8: i==maxEnd, partition, size=9, start=9
  • Continue similarly...

Output: [9, 7, 8]

Edge Cases

  • Single character: return [1]
  • All same character: return [s.length]
  • All unique characters: return [1, 1, 1, ...]