← All Guides

Greedy

9 drills · 9 pattern exercises · 18 micro drills

Patterns Overview

Patterns used in this topic, with difficulty breakdown.

Greedy Choice 5 drills 1 Easy 4 Med
BFS Greedy 1 drill 1 Med
Circular Scan 1 drill 1 Med
Last Occurrence 1 drill 1 Med
Min-Max Range 1 drill 1 Med

Foundation Drill

71 Best time to buy/sell stock Easy Greedy Choice

Find the best day to buy and the best day to sell a stock to make the most money.

Start here in Foundations →

Coding Drills

All 9 drills grouped by pattern. Each includes prompt, solution, and complexity.

Greedy Choice (5)

71 Best time to buy/sell stock Easy

In plain English: Find the best day to buy and the best day to sell a stock to make the most money.

Track the minimum price seen so far. At each day, the best you can do is sell at today's price minus the cheapest buy — one pass, O(1) space.

Prompt

Given an array of stock prices (one per day), find the maximum profit from one buy and one sell (buy before sell).

Solution

def max_profit(prices):
    min_price = float('inf')
    max_profit = 0
    for p in prices:
        min_price = min(min_price, p)
        max_profit = max(max_profit, p - min_price)  # sell today, bought at min
    return max_profit

# Test: max_profit([7,1,5,3,6,4]) == 5  (buy at 1, sell at 6)
# Test: max_profit([7,6,4,3,1]) == 0  (no profit possible)
O(n) time · O(1) space
72 Jump game Medium

In plain English: Starting at the first position, check if you can hop to the end given each position's maximum jump distance.

Track the farthest index reachable. At each position, update the farthest reach — if you ever land beyond the farthest, you are stuck.

Prompt

Given an array where each element is the max jump length from that position, determine if you can reach the last index starting from index 0.

Solution

def can_jump(nums):
    farthest = 0
    for i in range(len(nums)):
        if i > farthest:
            return False  # stuck: can't reach this position
        farthest = max(farthest, i + nums[i])
    return True

# Test: can_jump([2,3,1,1,4]) == True
# Test: can_jump([3,2,1,0,4]) == False
O(n) time · O(1) space
73 Gas station Medium

In plain English: Find which gas station to start at so you can drive around a circular route without running out of fuel.

If total gas >= total cost, a solution exists. Track the running surplus — whenever it goes negative, the answer must be past that point.

Prompt

There are n gas stations in a circle. Given gas[i] and cost[i], find the starting station index for a complete circuit, or -1 if impossible.

Solution

def can_complete_circuit(gas, cost):
    total = 0
    tank = 0
    start = 0
    for i in range(len(gas)):
        diff = gas[i] - cost[i]
        total += diff
        tank += diff
        if tank < 0:
            start = i + 1  # can't start at or before i
            tank = 0
    return start if total >= 0 else -1

# Test: can_complete_circuit([1,2,3,4,5],[3,4,5,1,2]) == 3
# Test: can_complete_circuit([2,3,4],[3,4,3]) == -1
O(n) time · O(1) space
74 Partition labels Medium

In plain English: Split a string into the most pieces possible where no letter appears in more than one piece.

Record each character's last occurrence. Extend the current partition's end to cover all characters' last positions — cut when you reach the end.

Prompt

Partition a string into as many parts as possible so that each letter appears in at most one part. Return the sizes of the parts.

Solution

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

# Test: partition_labels("ababcbacadefegdehijhklij") == [9, 7, 8]
O(n) time · O(1) space
75 Non-overlapping intervals Medium

In plain English: Remove the fewest intervals so that none of the remaining ones overlap.

Sort by end time and greedily keep intervals that start after the previous one ends — this maximizes non-overlapping intervals (classic activity selection).

Prompt

Given a list of intervals, find the minimum number of intervals to remove so that the remaining intervals do not overlap.

Solution

def erase_overlap_intervals(intervals):
    intervals.sort(key=lambda x: x[1])  # sort by end time
    count = 0
    prev_end = float('-inf')
    for start, end in intervals:
        if start >= prev_end:
            prev_end = end  # keep this interval
        else:
            count += 1  # remove this interval (overlaps)
    return count

# Test: erase_overlap_intervals([[1,2],[2,3],[3,4],[1,3]]) == 1
# Test: erase_overlap_intervals([[1,2],[1,2],[1,2]]) == 2
O(n log n) time · O(1) space

BFS Greedy (1)

147 Jump Game II Medium

In plain English: Find the fewest jumps needed to get from the start to the end of the array.

Think of it as BFS where each 'level' is the farthest you can reach in one more jump. Track the current level's end and the farthest reachable. When you pass the current end, increment jumps.

Prompt

Given an array where each element is the max jump length from that position, return the minimum number of jumps to reach the last index. Assume you can always reach the end.

Solution

def jump(nums):
    jumps = 0
    cur_end = 0
    farthest = 0
    for i in range(len(nums) - 1):
        farthest = max(farthest, i + nums[i])
        if i == cur_end:  # must jump
            jumps += 1
            cur_end = farthest
    return jumps

# Test: jump([2,3,1,1,4]) == 2  (0->1->4)
# Test: jump([2,3,0,1,4]) == 2
O(n) time · O(1) space

Circular Scan (1)

148 Gas Station Medium

In plain English: Find the gas station to start at so you can drive around a circular route without running out of fuel.

If total gas >= total cost, a solution exists. Track running tank — if it goes negative, the start must be past this point (reset). The greedy insight: the station after the worst deficit is the answer.

Prompt

There are n gas stations in a circle. gas[i] is the fuel at station i, cost[i] is fuel to travel to station i+1. Return the starting station index to complete the circuit, or -1 if impossible.

Solution

def can_complete_circuit(gas, cost):
    if sum(gas) < sum(cost):
        return -1
    tank = 0
    start = 0
    for i in range(len(gas)):
        tank += gas[i] - cost[i]
        if tank < 0:
            start = i + 1  # can't start at or before i
            tank = 0
    return start

# Test: can_complete_circuit([1,2,3,4,5], [3,4,5,1,2]) == 3
# Test: can_complete_circuit([2,3,4], [3,4,3]) == -1
O(n) time · O(1) space

Last Occurrence (1)

149 Partition Labels Medium

In plain English: Split a string into the most pieces possible where no letter appears in more than one piece.

First pass: record the last occurrence of each character. Second pass: expand the current partition's end to include the last occurrence of every character encountered. When you reach the end, cut.

Prompt

Given a string s, partition it into as many parts as possible so that each letter appears in at most one part. Return the sizes of the parts.

Solution

def partition_labels(s):
    last = {ch: i for i, ch in enumerate(s)}
    result = []
    start = end = 0
    for i, ch in enumerate(s):
        end = max(end, last[ch])  # extend partition
        if i == end:              # reached the boundary
            result.append(end - start + 1)
            start = i + 1
    return result

# Test: partition_labels("ababcbacadefegdehijhklij")
#       == [9, 7, 8]
# Test: partition_labels("eccbbbbdec") == [10]
O(n) time · O(1) space (26 letters)

Min-Max Range (1)

150 Valid Parenthesis String Medium

In plain English: Check if a string of parentheses with wildcards (*) can be made valid.

Track a range [lo, hi] of possible open-paren counts. '(' increments both, ')' decrements both, '*' widens the range. If hi < 0, too many ')'. If lo < 0, clamp to 0. Valid if lo == 0 at end.

Prompt

Given a string containing '(', ')' and '*' where '*' can be '(', ')' or empty, determine if the string is valid.

Solution

def check_valid_string(s):
    lo = hi = 0  # range of possible open paren counts
    for ch in s:
        if ch == '(':
            lo += 1
            hi += 1
        elif ch == ')':
            lo -= 1
            hi -= 1
        else:  # '*'
            lo -= 1  # treat as ')'
            hi += 1  # treat as '('
        if hi &lt; 0:    # too many ')'
            return False
        lo = max(lo, 0)  # can't have negative open count
    return lo == 0

# Test: check_valid_string("(*))") == True
# Test: check_valid_string("(*)") == True
# Test: check_valid_string(")(") == False
O(n) time · O(1) space

Pattern Recognition

9 exercises: spot the signals, pick the method, avoid the trap.

71 Best time to buy/sell stock Easy

Find maximum profit from one buy and one sell (buy before sell).

Signals

Greedy

Track min_price. At each day: max_profit = max(max_profit, price - min_price). O(n).

Dynamic Programming

DP formulation works but is overkill. Greedy with running min is simpler.

Trap

Brute force checking all (buy, sell) pairs is O(n²). The running minimum insight makes it O(n).

72 Jump game Medium

Can you reach the last index? Each element is the max jump length from that position.

Signals

Greedy

Track max_reach. At each position i: if i > max_reach, return False. Else update max_reach. O(n).

Dynamic Programming

dp[i] = can reach i? Works but O(n²). Greedy is O(n) because reachability is monotonic.

Trap

DP is correct but slow. The greedy insight: once a position is reachable, all earlier ones are too.

73 Gas station Medium

Find starting station for a complete circuit, or -1 if impossible.

Signals

Greedy

If total_gas >= total_cost, answer exists. Track running surplus; when < 0, reset start to next station. O(n).

Trap

Brute force trying every starting station is O(n²). The surplus reset insight gives O(n).

74 Partition labels Medium

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

Signals

Greedy

Map each char to its last index. Scan: extend partition end to max(last[c]). Cut when i == end. O(n).

Hash Map

The hash map is part of the greedy solution — used to precompute last occurrences.

Trap

This is an interval merging problem in disguise. Each character defines an interval [first, last].

75 Non-overlapping intervals Medium

Minimum intervals to remove so the rest don't overlap.

Signals

Greedy

Sort by end time. Keep intervals that don't overlap with the last kept. Remove count = total - kept. O(n log n).

Dynamic Programming

LIS-like DP on intervals sorted by end. O(n²) — much slower than greedy.

Trap

Sort by END time, not start time. Sorting by start doesn't give the optimal greedy choice.

147 Jump Game II Medium

Given array where each element is max jump length, find minimum jumps to reach the last index.

Signals

Greedy

Track current jump boundary and farthest reach. When hitting boundary, jump++, extend boundary. O(n).

Dynamic Programming

dp[i] = min jumps to reach i. O(n²) — much slower.

Trap

Don't BFS with a queue — the greedy two-pointer approach is O(n) with O(1) space.

148 Gas Station Medium

Given gas stations in a circle with gas[i] and cost[i], find the starting station to complete the circuit, or -1.

Signals

Greedy

Track total and current tank. If current < 0, reset start to next station. If total >= 0, start is valid. O(n).

Trap

Don't simulate from each station O(n²). The single-pass greedy works because if total >= 0, exactly one valid start exists.

149 Partition Labels Medium

Partition a string so each letter appears in at most one part. Maximize the number of parts.

Signals

Greedy

Track last occurrence of each char. Extend current partition to max(last[c]). When i == end, cut. O(n).

Hash Map

Same. The hash map of last occurrences is the key data structure.

Trap

Don't try to split greedily without precomputing last occurrences — you'd need to scan ahead repeatedly.

150 Valid Parenthesis String Medium

Given a string with '(', ')', and '*' (* can be '(', ')' or empty), determine if it's valid.

Signals

Greedy

Track lo (min open) and hi (max open). '(' → both++. ')' → both--. '*' → lo--, hi++. Valid if lo <= 0 <= hi throughout. O(n).

Dynamic Programming

dp[i][open_count] = feasibility. O(n²) — overkill when greedy works.

Trap

Don't generate all 3^n possibilities for *. The lo/hi range tracking is the elegant O(n) solution.

Related Micro Drills

18 quick recall drills to reinforce this topic.

167 Buy/Sell Stock with Fee Recursion & DP 10s

Two-state DP: cash (free to buy) and hold (free to sell). Fee paid on sell. No cooldown needed.

def maxProfit(prices, fee):
    cash = 0            # not holding
    hold = -prices[0]   # holding
    for p in prices[1:]:
        cash = max(cash, hold + p - fee)
        hold = max(hold, cash - p)
    return cash
166 Buy/Sell Stock with Cooldown Recursion & DP 15s

Three-state machine: hold → sell → rest → buy. After selling, must rest one day before buying.

def maxProfit(prices):
    hold = -prices[0]  # holding stock
    sold = 0           # just sold (cooldown next)
    rest = 0           # not holding, free to buy
    for p in prices[1:]:
        hold, sold, rest = (
            max(hold, rest - p),   # keep or buy
            hold + p,              # sell
            max(rest, sold),       # stay or come off cooldown
        )
    return max(sold, rest)
102 Track min price, max profit Sliding Window 10s

Single-pass tracking of running minimum and maximum profit. Classic greedy O(n) solution.

mn = float('inf')
mx = 0
for p in prices:
    mn = min(mn, p)
    mx = max(mx, p - mn)
147 Jump game II (greedy BFS) Pointer Manipulation 10s

Track farthest reachable and current boundary. Increment jumps when boundary is hit.

jumps = cur_end = farthest = 0
for i in range(len(nums) - 1):
    farthest = max(farthest, i + nums[i])
    if i == cur_end:
        jumps += 1
        cur_end = farthest
87 Move zeros to end Pointer Manipulation 10s

Move-zeroes is remove-element followed by a fill. Classic two-pointer warm-up.

w = 0
for r in range(len(a)):
    if a[r] != 0:
        a[w] = a[r]
        w += 1
while w &lt; len(a):
    a[w] = 0
    w += 1
126 Remove duplicates in-place Pointer Manipulation 10s

Write-pointer compaction for sorted arrays. Returns new length.

w = 1
for i in range(1, len(a)):
    if a[i] != a[i-1]:
        a[w] = a[i]
        w += 1
return w
148 Gas station circular Pointer Manipulation 15s

Track running surplus and total balance. Reset start when tank goes negative.

total = tank = start = 0
for i in range(len(gas)):
    diff = gas[i] - cost[i]
    total += diff
    tank += diff
    if tank &lt; 0:
        start = i + 1
        tank = 0
return start if total &gt;= 0 else -1
149 Partition labels (last occurrence) Hash Strategies 10s

Map each char to its last index. Extend partition end greedily.

last = {c: i for i, c in enumerate(s)}
res = []
start = end = 0
for i, c in enumerate(s):
    end = max(end, last[c])
    if i == end:
        res.append(end - start + 1)
        start = i + 1
76 Merge two sorted lists Divide & Conquer 15s

Merging sorted lists is the core of merge sort and merge-k-lists.

i = j = 0
merged = []
while i &lt; len(a) and j &lt; len(b):
    if a[i] &lt;= b[j]:
        merged.append(a[i]); i += 1
    else:
        merged.append(b[j]); j += 1
merged.extend(a[i:])
merged.extend(b[j:])
104 Longest substring no repeats Sliding Window 15s

Hash-map window tracks last index of each char. Avoids set removal on duplicates.

seen = {}
l = res = 0
for r, c in enumerate(s):
    if c in seen and seen[c] &gt;= l:
        l = seen[c] + 1
    seen[c] = r
    res = max(res, r - l + 1)
73 Sort with key function Divide & Conquer 5s

Custom key sorting is Python's primary sort mechanism. Foundation for interval and custom-order problems.

a.sort(key=lambda x: x[1])
74 Sort by multiple keys Divide & Conquer 10s

Multi-key sorting handles tiebreakers. Used in leaderboard and event scheduling.

a.sort(key=lambda x: (x[0], -x[1]))
115 Sort by end time Interval & Math 5s

Sorting by end time is the greedy strategy for interval scheduling and activity selection.

intervals.sort(key=lambda x: x[1])
9 Partition around pivot (Lomuto) Pointer Manipulation 15s

Lomuto partition is the core of quicksort and quickselect. Know it cold for kth-element problems.

def partition(a, lo, hi):
    pivot = a[hi]
    i = lo
    for j in range(lo, hi):
        if a[j] &lt;= pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[hi] = a[hi], a[i]
    return i
77 Partition step (quicksort) Divide & Conquer 15s

Partition is the heart of quicksort. Same as Lomuto partition — drill until automatic.

def partition(a, lo, hi):
    pivot = a[hi]
    i = lo
    for j in range(lo, hi):
        if a[j] &lt;= pivot:
            a[i], a[j] = a[j], a[i]
            i += 1
    a[i], a[hi] = a[hi], a[i]
    return i
128 LCS bottom-up Recursion & DP 15s

Longest Common Subsequence is a classic 2D DP. Foundation for diff algorithms.

dp = [[0]*(n+1) for _ in range(m+1)]
for i in range(1, m+1):
    for j in range(1, n+1):
        if a[i-1] == b[j-1]:
            dp[i][j] = dp[i-1][j-1] + 1
        else:
            dp[i][j] = max(dp[i-1][j], dp[i][j-1])
160 Longest Palindromic Subsequence Recursion & DP 15s

2D interval DP. Iterate i from bottom-up so dp[i+1] is already computed. Equivalent to LCS(s, reverse(s)).

def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0] * n for _ in range(n)]
    for i in range(n - 1, -1, -1):
        dp[i][i] = 1
        for j in range(i + 1, n):
            if s[i] == s[j]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    return dp[0][n-1]
266 Frequency window (longest repeating replacement) Sliding Window 15s

Window is valid when (window_size - max_freq) <= k. max_freq only needs to increase — shrinking the window can't help.

count = {}
l = max_freq = 0
for r in range(len(s)):
    count[s[r]] = count.get(s[r], 0) + 1
    max_freq = max(max_freq, count[s[r]])
    if (r - l + 1) - max_freq &gt; k:
        count[s[l]] -= 1
        l += 1