Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
What is Greedy?
Greedy algorithms make the locally optimal choice at each step, hoping to find a global optimum. They work when local choices lead to global solutions (greedy choice property + optimal substructure). Imagine you are at a buffet and want to fill your plate with the most delicious items, but you can only visit the buffet line once (no going back). A greedy strategy is: at each station, take the best item available. This works if the items are independent. But if taking the salad now means you will not have room for the steak later, greedy fails — you need DP to consider all possibilities. The critical question for any greedy problem is: does making the locally best choice now ever prevent a globally better outcome later? If the answer is “no” (backed by a proof or strong intuition), greedy works. If “yes” or “maybe,” use DP. Two properties must hold:- Greedy choice property: A globally optimal solution can be built by making locally optimal choices.
- Optimal substructure: An optimal solution contains optimal solutions to subproblems.
Pattern Recognition Checklist
Use Greedy When
- Interval scheduling/merging
- Minimum coins (certain denominations)
- Jump game style problems
- Huffman encoding
- Activity selection
- Fractional knapsack
Don't Use When
- 0/1 Knapsack (need DP)
- Coin change (arbitrary denominations)
- Need to try all possibilities
- Local optimal may miss global
- Problem has overlapping subproblems
Greedy vs DP: How to Choose
| Characteristic | Greedy | DP |
|---|---|---|
| Makes irrevocable choices | Yes | No, tries all |
| Optimal substructure | Required | Required |
| Overlapping subproblems | Not handled | Handled via caching |
| Time complexity | Usually O(n log n) | Usually O(n^2) or more |
| Proof required | Yes (exchange argument) | No, exhaustive |
Common Greedy Patterns
| Pattern | Greedy Choice | Example |
|---|---|---|
| Interval Scheduling | Sort by end time | Non-overlapping intervals |
| Interval Partitioning | Sort by start time | Meeting rooms |
| Fractional Knapsack | Best value/weight ratio | Maximize value |
| Job Scheduling | Sort by deadline | Minimize late jobs |
| Huffman Coding | Merge least frequent | Optimal prefix codes |
When to Use
Scheduling
Selection
Graph Problems
String/Array
Pattern Variations
1. Interval Scheduling
The classic greedy problem. The key insight: sort by end time, then greedily pick the earliest-ending interval that does not overlap the previous selection. Why end time, not start time? An interval that ends early leaves the most room for future intervals — it is the least “greedy” choice in terms of consuming the timeline. Proof sketch (exchange argument): If an optimal solution picks an interval that ends later than our greedy choice, we can swap it with the greedy choice without reducing the count (the greedy choice ends earlier, so everything after still fits). Time: O(n log n) for sorting. Space: O(1) (if sorting in place). Interview variation: “Minimum number of intervals to remove to make the rest non-overlapping” is justn - max_non_overlapping. Same algorithm, different framing.
2. Activity Selection (Meeting Rooms)
How many meeting rooms do you need so that no two overlapping meetings share a room? The key insight: instead of simulating room assignments, think of it as counting the maximum number of meetings happening at any single point in time. That peak overlap is the answer. The sweep-line trick: Separate start times and end times into two sorted arrays, then sweep through them with two pointers. A start means one more room is needed; an end means one room is freed. The maximum rooms in use at any point is the answer. Why this works: We do not care which meeting starts or ends — only when. Sorting starts and ends independently is valid because we only need the count of simultaneous events, not their pairing. Walked example: meetings = [[0,30], [5,10], [15,20]]. Starts: [0, 5, 15]. Ends: [10, 20, 30]. At time 0: +1 room (1). At time 5: +1 room (2). At time 10: -1 room (1). At time 15: +1 room (2). Peak = 2 rooms. Time: O(n log n) for sorting. Space: O(n) for the separated arrays.3. Jump Game
Jump Game I (can you reach the end?): Track the farthest reachable index. If at any point your current index exceedsmax_reach, you are stuck. This is O(n) time, O(1) space.
Jump Game II (minimum jumps): A BFS-like greedy approach. Imagine BFS levels where each “level” is the range of indices reachable with one more jump. current_end marks where the current level ends; farthest tracks the farthest we can reach from this level. When we reach current_end, we take a jump and extend to farthest.
Walked example (Jump Game II): nums = [2,3,1,1,4]. Level 0: index 0, can reach up to 2. Level 1: indices 1-2, can reach up to 4. We reached the end in 2 jumps.
Time: O(n). Space: O(1) for both variants.
4. Gas Station
There aren gas stations on a circular route. Station i gives you gas[i] fuel, and it costs cost[i] fuel to drive to the next station. Find the starting station from which you can complete the full circuit, or return -1 if impossible.
Two key insights:
- Global feasibility: If
sum(gas) >= sum(cost), a solution must exist. The total fuel available is enough for the total cost, so some starting point works. - Local elimination: If starting from station
s, you run dry at stationi, then no station betweensandican be a valid start either. Why? Because you arrived at each intermediate station with non-negative fuel (otherwise you would have failed earlier), so starting from any of them with zero fuel would fail even sooner.
5. Partition Labels
Partition a string into as many parts as possible so that each letter appears in at most one part. This is secretly an interval merging problem in disguise. The key reframing: Each characterc defines an interval [first_occurrence(c), last_occurrence(c)]. The constraint “each letter in at most one part” means no character’s interval can be split across partitions. So the problem becomes: merge all overlapping character intervals and report the merged segment sizes.
The single-pass trick: Scan left to right, expanding end to cover the last occurrence of every character seen so far. When i == end, all characters in the current partition have their last occurrences within [start, end] — safe to cut.
Walked example: s = “ababcbacadefegdehijhklij”. Character a spans [0,8], b spans [1,5], c spans [4,7], d spans [9,14], e spans [10,15], etc. First merged interval: [0,8] (length 9). Second: [9,15] (length 7). Third: [16,23] (length 8). Result: [9, 7, 8].
Time: O(n) — two passes (one for last-occurrence map, one for scanning). Space: O(1) extra (the map has at most 26 entries for lowercase letters).
Classic Problems
1. Interval Scheduling
1. Interval Scheduling
2. Jump Game
2. Jump Game
3. Task Scheduler
3. Task Scheduler
4. Candy Distribution
4. Candy Distribution
Common Mistakes
How to Prove Greedy Works
Interview Problems by Company
- Easy
- Medium
- Hard
| Problem | Company | Key Concept |
|---|---|---|
| Assign Cookies | Amazon | Sort both arrays |
| Lemonade Change | Track denominations | |
| Best Time to Buy Stock | All FAANG | Track minimum so far |
Interview Tips
How to Explain Your Approach
How to Explain Your Approach
- “This looks like a greedy problem because local choices lead to global optimum.”
- “My greedy strategy is: [describe the choice]”
- “This works because [brief justification]”
- “I’ll sort by [criteria] first, then iterate greedily.”
- “Time is O(n log n) for sorting, O(n) for the greedy pass.”
When Interviewer Says...
When Interviewer Says...
| Interviewer Says | You Should Think |
|---|---|
| ”Minimum operations” | Greedy or DP |
| ”Schedule tasks/meetings” | Interval scheduling |
| ”Maximum profit/value” | Greedy or DP |
| ”Prove it’s optimal” | Exchange argument |
| ”What if greedy fails?” | Consider DP |
Greedy Template
Greedy Template
Greedy Templates
Two reusable shapes you will write again and again. Memorize the structure, fill in the comparator and update.Template 1: Sort Plus One Pass
Template 2: Single-Pass Tracker (Jump Game style)
Counter-Pattern — Where Greedy Fails
The classic counterexample: minimum number of coins with arbitrary denominations.Worked LeetCode Problems
Six staple greedy problems. If you can write each of these from scratch in eight minutes, you have the pattern.LC 55 — Jump Game (Medium)
Pattern: Single-pass tracker — maintain the farthest index reachable so far.i, the maximum reach is i + nums[i]. If any reachable index updates farthest to at least n - 1, we are done. We never have to consider “what if I had jumped less here so I could jump more later” because the only thing that matters is the maximum reach, and a larger jump at the current index can never hurt.
LC 45 — Jump Game II (Medium)
Pattern: BFS-flavored greedy — count the layers of “current reach window.”LC 134 — Gas Station (Medium)
Pattern: Two-pointer-flavored greedy with a clever invariant.start and i, then no index in [start, i] can be a valid start (each one would have less or equal tank when reaching i). Skip them all and try i + 1. One pass.
LC 435 — Non-overlapping Intervals (Medium)
Pattern: Sort by end time, count compatible intervals, subtract.LC 621 — Task Scheduler (Medium)
Pattern: Formula based on the most frequent task — no simulation needed for the standard variant.n + 1 apart: A _ _ A _ _ A. That is (max_freq - 1) * (n + 1) + 1. If multiple tasks tie for the top frequency, each one extends the final row by 1 slot, hence + count_max. The max(len(tasks), formula) guards the case where tasks already fill all slots without idle time.
LC 763 — Partition Labels (Medium)
Pattern: Pre-compute last-occurrence index, then expand the current partition.i must extend at least to last[c] for every c seen. When i finally equals end, the partition is closed — no character inside it appears later.
Curated LeetCode List
Sorted by difficulty and sub-pattern. Solve in this order; each one reinforces a different greedy idea.| LC # | Problem | Difficulty | Sub-pattern |
|---|---|---|---|
| 860 | Lemonade Change | Easy | Greedy by denomination |
| 122 | Best Time to Buy and Sell Stock II | Easy | Sum positive deltas |
| 55 | Jump Game | Medium | Single-pass reach tracker |
| 45 | Jump Game II | Medium | BFS-layer greedy |
| 134 | Gas Station | Medium | Skip-to-next-valid-start |
| 435 | Non-overlapping Intervals | Medium | Sort by end time |
| 452 | Burst Balloons (Min Arrows) | Medium | Sort by end, count overlaps |
| 621 | Task Scheduler | Medium | Frequency formula |
| 763 | Partition Labels | Medium | Expand to last occurrence |
| 1029 | Two City Scheduling | Medium | Sort by cost-A minus cost-B |
Practice Problems
| Problem | Difficulty | Link |
|---|---|---|
| Jump Game | Medium | LeetCode 55 |
| Gas Station | Medium | LeetCode 134 |
| Partition Labels | Medium | LeetCode 763 |
| Task Scheduler | Medium | LeetCode 621 |
| Non-overlapping Intervals | Medium | LeetCode 435 |
Practice Roadmap
Day 2: Interval Problems
- Solve: Non-overlapping Intervals, Meeting Rooms
- Focus: Sort by end time pattern
Interview Questions
Deep-dive questions that test real understanding of the Greedy pattern, not just recognizing “sort then iterate.” Expect interviewers to challenge your correctness proofs and push you toward edge cases where greedy breaks.Q1: In interval scheduling, why do we sort by end time and not start time? What breaks if you sort by start time instead?
Q1: In interval scheduling, why do we sort by end time and not start time? What breaks if you sort by start time instead?
- The greedy goal is to leave as much room as possible for future intervals. An interval that ends earliest frees up the timeline the most. Sorting by start time tells you nothing about when an interval finishes — an interval starting at time 1 could end at time 1000, blocking everything.
- Concrete counterexample: Intervals
[1,10], [2,3], [4,5]. Sorted by start time, you pick[1,10]first and can fit nothing else (1 interval). Sorted by end time:[2,3], [4,5], [1,10]— you pick[2,3], then[4,5](2 intervals). The start-time-first approach greedily chose an interval that monopolized the timeline. - Sorting by end time works because of the exchange argument: if the optimal solution picks an interval that does not end earliest, you can swap it for the earliest-ending interval without losing any capacity for future selections (the earliest-ending interval frees up at least as much space). So greedy stays ahead of or ties optimal at every step.
- Edge nuance: Sorting by start time does work for a different problem — interval partitioning (minimum meeting rooms), where you process intervals in order of arrival and allocate rooms. Different greedy problems demand different sorting keys.
- What if you need to minimize the number of intervals to remove to make the rest non-overlapping? Does the same greedy choice apply? (Answer: yes, it is the complement problem. Maximize non-overlapping = minimize removals. Same sort by end time.)
- What if intervals have weights and you want to maximize total weight of non-overlapping intervals? Does greedy still work? (Answer: no. This is the Weighted Job Scheduling problem, which requires DP. Greedy by end time ignores weights, and greedy by weight ignores overlaps. You need
dp[i] = max(dp[i-1], weight[i] + dp[last_compatible[i]])with binary search.)
Q2: Explain the exchange argument proof technique for greedy algorithms. Walk me through it for the activity selection problem.
Q2: Explain the exchange argument proof technique for greedy algorithms. Walk me through it for the activity selection problem.
- The exchange argument works in three steps: (1) Assume an optimal solution exists. (2) Show you can exchange one of optimal’s choices for the greedy choice without making the solution worse. (3) Repeat until optimal matches greedy, proving greedy is also optimal.
- For activity selection: Let
G = {g1, g2, ...}be greedy’s selections (sorted by end time) andO = {o1, o2, ...}be some optimal solution. Supposeo1 != g1. Since greedy picks the earliest-ending activity,g1.end <= o1.end. Replaceo1withg1in O. No activity in O overlapped witho1, and sinceg1ends even earlier,g1cannot overlap them either. SoO' = {g1, o2, o3, ...}is still valid and has the same size. Repeat foro2vsg2, and so on. - The critical realization: The exchange argument does not prove greedy finds the only optimal solution — it proves greedy finds an optimal solution. There may be multiple optimal solutions.
- When exchange argument breaks: If the “exchange” makes the solution worse (e.g., weighted intervals where swapping a high-weight interval for the earliest-ending low-weight one reduces total value), you know greedy is incorrect for that problem. This is your litmus test: can you swap without damage?
- What is the “greedy stays ahead” proof technique and how does it differ from exchange? (Answer: “greedy stays ahead” shows that after each step, greedy’s partial solution is at least as good as any other algorithm’s partial solution. It is more direct but requires defining a clear measure of “ahead.” Exchange works by modifying the optimal solution to match greedy.)
- Can you sketch a quick exchange argument for why Huffman coding produces the optimal prefix code? (Answer: if two least-frequent characters are not siblings at the maximum depth, swap them with the actual deepest siblings. Since they have the lowest frequency, pushing them deeper cannot increase total cost. So the greedy choice of combining least-frequent first is safe.)
Q3: In the Jump Game problem, why does tracking the maximum reachable index work? What invariant is being maintained?
Q3: In the Jump Game problem, why does tracking the maximum reachable index work? What invariant is being maintained?
max_reach loop but cannot explain why it is correct or what the loop invariant is. This also tests whether a candidate thinks about problem properties (is the problem even greedy?) or jumps straight to coding.Strong answer:- The invariant: After processing index
i,max_reachstores the farthest index reachable using any combination of jumps from indices0..i. If at any pointi > max_reach, there is a “gap” we cannot cross, so we return False. - Why greedy works here: At each index, you do not need to decide which specific jump to take — you just need to know whether the current index is reachable and how far you can reach from here. The maximum reachable index is monotonically non-decreasing as you scan left to right, so the “best local information” (maximum reach from any visited index) is also globally sufficient.
- Why this is greedy, not DP: There are no overlapping subproblems. You never need to revisit an index. A single left-to-right scan with a running max is sufficient. DP would be O(n^2) (for each position, check all previous positions that can reach it), but the greedy observation that
max_reachonly grows eliminates that. - Key insight for Jump Game II (min jumps): Extend the idea with a “current level end” boundary. Every time you reach the boundary, you must take a jump. Track the farthest you can reach within the current level. This is essentially BFS in disguise, where each “level” is one jump.
max_reach is reachable), they will not be able to adapt this approach to variations like Jump Game II or jump-with-cost problems.Follow-ups:- Jump Game II asks for the minimum number of jumps. How does the greedy approach change, and why is it BFS-like? (Answer: maintain
current_endandfarthest. When you hitcurrent_end, increment jumps and setcurrent_end = farthest. Each “level” between boundaries represents one jump, just like BFS levels.) - What if each jump has a cost and you want to minimize total cost to reach the end? Does greedy still work? (Answer: no. Greedy by max-reach ignores cost, and greedy by min-cost ignores reachability. You need DP:
dp[i] = min(dp[j] + cost[j])for alljthat can reachi. Or use a min-heap for an O(n log n) approach.)
Q4: Walk me through the Gas Station problem. Why does resetting the start index when the tank goes negative produce the correct answer?
Q4: Walk me through the Gas Station problem. Why does resetting the start index when the tank goes negative produce the correct answer?
- The algorithm: Track
total_tank(global feasibility) andcurrent_tank(local feasibility from currentstart). Ifcurrent_tankdrops below 0 at stationi, setstart = i + 1and resetcurrent_tank = 0. - Why skipping stations is safe: If starting from station
s, you run out of gas at stationi, then no station between s and i (inclusive) can be a valid start. Here is why: stationswas chosen because it had positive or zero net gas. As you drove froms, the cumulative gas was non-negative at every intermediate stationk(otherwise you would have failed earlier atk). Since even with that “head start” of accumulated gas froms..k-1, you still failed ati, starting fresh fromk(with zero accumulated gas) would fail even sooner. - Global feasibility check:
total_tank >= 0at the end guarantees a solution exists. If the total gas >= total cost, there must be some starting point that works (this follows from the circular nature of the problem — the deficit from the worst segment is compensated by surplus elsewhere). - Why the last
startis correct: After the final reset, every subsequent station fromstartton-1is reachable (otherwise another reset would have occurred). And we know the total is non-negative, so the deficit from stations0..start-1is covered by the surplus fromstart..n-1carried around.
- What if there can be multiple valid starting stations? Does this algorithm find all of them or just one? (Answer: it finds one. The problem guarantees uniqueness if a solution exists. If uniqueness is not guaranteed, you would need to test each candidate start in O(n), giving O(n^2) worst case, or use more sophisticated analysis.)
- What if the circuit is not circular but linear (start at 0, reach station n-1)? Does the same approach work? (Answer: simpler — just scan left to right. If
current_tankever goes negative, there is no solution. No need for the circular logic ortotal_tankcheck.)
Q5: The Fractional Knapsack is greedy, but the 0/1 Knapsack requires DP. What fundamental property makes greedy fail for 0/1 Knapsack?
Q5: The Fractional Knapsack is greedy, but the 0/1 Knapsack requires DP. What fundamental property makes greedy fail for 0/1 Knapsack?
- The core difference: In Fractional Knapsack, you can take a fraction of an item, so the greedy choice (best value-per-weight ratio) is always safe — taking a fraction of the best item never blocks a better overall solution. In 0/1 Knapsack, you must take or leave the entire item, so a high-ratio item might consume capacity that could have been better used by a combination of smaller items.
- Concrete counterexample: Capacity = 10. Items: A (weight 6, value 6, ratio 1.0), B (weight 5, value 5, ratio 1.0), C (weight 5, value 5, ratio 1.0). Greedy by ratio picks A first (or either, since ratios are equal), leaving capacity 4 — cannot fit B or C. Total value: 6. Optimal: take B + C = value 10, weight 10. Greedy missed it because taking A blocked the better combination.
- The broken property: Greedy requires the greedy choice property — that making the locally optimal choice does not foreclose a globally optimal solution. Fractional Knapsack has this property (you can always “undo” a greedy choice by taking slightly less). 0/1 Knapsack does not (you cannot undo taking a whole item without removing it entirely, which changes the solution structure).
- The DP solution uses overlapping subproblems:
dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i]). This exhaustively considers both “take” and “skip” for each item, which greedy does not do.
- What about the Unbounded Knapsack (unlimited copies of each item)? Can greedy work there? (Answer: still no. The same type of counterexample applies — a high-ratio item might waste capacity if it does not divide evenly into the remaining space. Unbounded Knapsack uses a different DP recurrence:
dp[w] = max(dp[w], dp[w-weight[i]] + value[i])for all items.) - Is there a special case of 0/1 Knapsack where greedy does work? (Answer: yes, when all items have the same weight, or when the value is proportional to weight. In these degenerate cases, the greedy choice property holds because no trade-off between ratio and capacity exists.)
Q6: In the Partition Labels problem, why does tracking the last occurrence of each character and expanding the partition window produce the minimum number of partitions?
Q6: In the Partition Labels problem, why does tracking the last occurrence of each character and expanding the partition window produce the minimum number of partitions?
- Reframe as an interval problem: Each character
cin the string defines an interval[first_occurrence(c), last_occurrence(c)]. The constraint “each letter appears in at most one part” means these intervals cannot be split across partitions. The problem reduces to: merge all overlapping character intervals, then count the merged intervals. - Why the single-pass algorithm works: As you scan left to right,
end = max(end, last[c])is essentially merging the current character’s interval into the growing partition. Wheni == end, all character intervals that started in this partition have also ended — no character in this partition appears later. That is exactly the condition for a valid partition boundary. - Why it is minimum: You cut at the earliest possible position each time (as soon as
i == end). Delaying a cut would only merge more characters into the current partition, reducing the total number of partitions. Since we cut as early as possible, we maximize the number of partitions, which means we minimize partition sizes — but actually, we maximize partition count, which is the same as producing the minimum-size partitions that satisfy the constraint. - Time/Space: O(n) time (two passes: one for last-occurrence map, one for scanning). O(1) extra space (the last-occurrence map has at most 26 entries for lowercase letters).
i == end is correct (not just “it seems right”), they lack the deeper understanding. Another red flag: not realizing this produces the maximum number of partitions (not just any valid partition).Follow-ups:- What if the constraint changes to “each letter appears in at most TWO parts”? How does the approach change? (Answer: significantly harder. You would need to track not just the last occurrence but allow intervals to be split once. This likely requires DP or a more complex interval analysis — the clean greedy approach breaks.)
- Can you solve this problem using an explicit interval-merging approach and verify it gives the same result? (Answer: yes. Build intervals
[first[c], last[c]]for each character, merge overlapping intervals, and count. It is O(n + 26 log 26) which simplifies to O(n). Same result, but the direct single-pass approach is cleaner for an interview.)
Q7: You are given a list of tasks with deadlines and penalties. Each task takes one unit of time. Maximize the number of tasks completed before their deadline. How do you approach this?
Q7: You are given a list of tasks with deadlines and penalties. Each task takes one unit of time. Maximize the number of tasks completed before their deadline. How do you approach this?
- Greedy strategy: Sort tasks by penalty (or profit) in decreasing order. For each task, schedule it in the latest available time slot before its deadline. If no slot is available, skip the task.
- Why latest-available slot: Scheduling a task as late as possible before its deadline preserves earlier time slots for tasks with earlier deadlines. If you greedily assign tasks to their earliest available slot, you might block a higher-penalty task that needed that slot.
- Implementation with Union-Find: The naive approach (for each task, scan backward from deadline to find an open slot) is O(n^2). Using a Union-Find (Disjoint Set) structure where each time slot points to the next available slot, you can find the latest open slot in near O(1) amortized time. Total: O(n log n) for sorting + O(n * alpha(n)) for scheduling, which is effectively O(n log n).
- Concrete example: Tasks:
{A: deadline 2, penalty 100},{B: deadline 1, penalty 50},{C: deadline 2, penalty 30}. Sort by penalty: A, B, C. Schedule A in slot 2 (latest before deadline 2). Schedule B in slot 1 (latest before deadline 1). C wants slot 2 or 1, both taken — skip. Total: 150. If we had greedily assigned A to slot 1 (earliest), B would have no slot, and we would get A + C = 130. Wrong.
- How would you optimize the slot-finding step from O(n) per task to near O(1)? (Answer: Union-Find. Each slot points to itself initially. When a slot is used, union it with
slot - 1. Finding the latest available slot is afind(deadline)operation that returns the root of the component, which is the latest open slot.) - What if tasks have different durations (not all unit time)? Does the greedy approach still work? (Answer: no. Variable durations make this a variant of the scheduling problem that is NP-hard in general. You would need DP or approximation algorithms depending on the specific constraints.)
Q8: In the Task Scheduler problem (LC 621), why does the formula (max_freq - 1) * (n + 1) + count_of_max_freq_tasks work? What happens when it doesn't?
Q8: In the Task Scheduler problem (LC 621), why does the formula (max_freq - 1) * (n + 1) + count_of_max_freq_tasks work? What happens when it doesn't?
- Visualization: Imagine placing the most frequent task (say
Aappearsmax_freqtimes) into a grid. You createmax_freq - 1“gaps” of sizenbetween consecutive A’s (the cooldown). The formula(max_freq - 1) * (n + 1)counts the slots for all gaps plus the A’s that start them. Then addcount_of_max_freq_tasksfor the final row (tasks tied for highest frequency). - Example: Tasks
A A A B B B, n=2. Grid:A _ _ | A _ _ | A. Gaps = 2, each size n+1=3, plus final row with A and B = 2. Total =2*3 + 2 = 8. Fill in B’s:A B _ A B _ A B. Length 8. - When the formula “breaks”: When there are enough distinct tasks to fill all idle slots with no idle time remaining. In that case,
total_tasks > formula_result, and the answer is simplylen(tasks). The actual answer ismax(len(tasks), formula_result). - Why this is greedy: The greedy insight is that the most frequent task is the bottleneck. All other tasks fit into the gaps created by the most frequent task. By processing the highest-frequency task first, you determine the structure, and everything else fills in.
max(len(tasks), formula) — this means they do not understand what happens when idle time disappears.Follow-ups:- What if the cooldown period
nis 0? Does the formula still work? (Answer: yes.(max_freq - 1) * 1 + count_max = max_freq - 1 + count_max. Butmax(len(tasks), this)=len(tasks)since no idle is ever needed. The formula gracefully degrades.) - What if instead of a cooldown period, two specific tasks cannot be adjacent (like a graph coloring constraint)? Does this greedy approach generalize? (Answer: no. This becomes a graph-based scheduling problem. The simple frequency-based formula assumes all tasks have the same mutual cooldown, which is a uniform constraint. Pairwise constraints require chromatic scheduling or interval coloring techniques.)
Q9: The Candy Distribution problem requires two passes (left-to-right, then right-to-left). Why is a single pass insufficient? Can you prove two passes are enough?
Q9: The Candy Distribution problem requires two passes (left-to-right, then right-to-left). Why is a single pass insufficient? Can you prove two passes are enough?
- The problem: Each child gets at least 1 candy. If a child has a higher rating than a neighbor, they must get more candies. Minimize total candies.
- Why one pass fails: A left-to-right pass enforces “if
rating[i] > rating[i-1], thencandy[i] > candy[i-1].” But it cannot enforce the reverse: “ifrating[i] > rating[i+1], thencandy[i] > candy[i+1]” because you have not processedi+1yet. Example: ratings[1, 3, 2]. Left-to-right gives[1, 2, 1]. Child at index 1 (rating 3) has more candy than child at index 2 (rating 2) — correct. But consider[1, 2, 3, 1]: left-to-right gives[1, 2, 3, 1], which is correct by luck. Now try[3, 2, 1]: left-to-right gives[1, 1, 1]— wrong, because each child should have more than the next. - Two passes work because: Pass 1 (left-to-right) enforces all “left neighbor” constraints. Pass 2 (right-to-left) enforces all “right neighbor” constraints. At each position, take
max(left_pass[i], right_pass[i])to satisfy both. Since each constraint is between adjacent pairs, and we enforce all leftward constraints in one pass and all rightward constraints in another, two passes cover all constraints. - Proof of minimality: Each child gets exactly the minimum candies needed to satisfy both neighbors. The
maxoperation ensures both constraints are met, and neither pass assigns more than necessary (each starts from 1 and increments by exactly 1 per consecutive increase).
- Can you solve this in a single pass using a different approach (not left-right greedy)? (Answer: yes, using a stack-based or “slope counting” approach that tracks ascending and descending sequences. It is O(n) time and O(1) extra space but significantly harder to implement correctly. The two-pass approach is almost always the better interview choice.)
- What if the constraint is “strictly more candies than BOTH neighbors if rating is higher than BOTH”? Does the two-pass approach still work? (Answer: yes. The two-pass approach already handles this. Each pass independently ensures the correct relationship with one neighbor, and the
maxcombines them. The constraint “higher than both” is just the intersection of “higher than left” and “higher than right.”)
Q10: How do you decide whether a new optimization problem is solvable with greedy or requires DP? Walk me through your decision process on an unfamiliar problem.
Q10: How do you decide whether a new optimization problem is solvable with greedy or requires DP? Walk me through your decision process on an unfamiliar problem.
- My decision tree:
- Can I make an irrevocable choice at each step without needing to reconsider? If yes, greedy is a candidate. If I might need to “undo” a choice later, that is a sign I need DP.
- Does the problem have optimal substructure? Both greedy and DP require this. If subproblems are not independent (solving one affects another), neither works cleanly.
- Does the problem have overlapping subproblems? If yes, DP. If subproblems are independent and each is solved exactly once, greedy.
- Can I construct a counterexample where the greedy choice fails? I spend 2-3 minutes trying to break the greedy approach with small inputs. If I find a counterexample, it is DP (or backtracking). If I cannot, greedy is likely correct.
- Can I sketch an exchange argument? If I can informally argue “swapping the greedy choice for any other choice does not improve the solution,” I am confident in greedy.
- Practical heuristic: If the problem says “minimum number of X” and the choices are independent (like interval scheduling), try greedy first. If it says “minimum cost” and choices have dependencies (like knapsack), lean toward DP.
- Real example of the process: “Minimum coins to make change.” Greedy thought: always pick the largest coin. Counterexample: coins
[1, 3, 4], target 6. Greedy picks 4+1+1=3 coins. Optimal: 3+3=2 coins. Greedy fails. Conclusion: DP. - Another example: “Minimum number of intervals to remove for non-overlap.” Greedy thought: sort by end time, keep non-overlapping. Can I break it? No — the exchange argument works (earliest end frees the most space). Conclusion: greedy.
- Can a problem have both a correct greedy solution and a correct DP solution? Give an example. (Answer: yes. Activity selection can be solved by DP with
dp[i] = max activities from i..n, but greedy is simpler and faster. The DP is correct but unnecessary. Fractional Knapsack similarly has a DP formulation, but greedy is optimal and more efficient.) - What about problems where greedy gives a good approximation but not the optimal answer? When would you use greedy anyway? (Answer: in practice, often. Set Cover is NP-hard, but the greedy algorithm gives a
ln(n)-approximation. Traveling Salesman with nearest-neighbor heuristic gives a quick 2x-approximation. In production systems with latency constraints, a greedy approximation that runs in O(n log n) can be far more valuable than an exact DP solution that runs in O(2^n).)
Interview Deep-Dive
How do you prove a greedy algorithm is correct? Walk through the exchange argument technique for interval scheduling.
How do you prove a greedy algorithm is correct? Walk through the exchange argument technique for interval scheduling.
- The exchange argument has three steps: (1) Assume an optimal solution O exists. (2) Show swapping one of O’s choices for greedy’s choice produces a solution at least as good. (3) Repeat until O matches greedy, proving greedy is optimal.
- For interval scheduling: Greedy sorts by end time and picks the earliest-ending non-overlapping interval. Let g1 be greedy’s first pick and o1 be optimal’s first pick. Since g1 ends earliest,
g1.end <= o1.end. Replace o1 with g1 in O. Since g1 ends no later than o1, it cannot overlap with O’s remaining intervals. So O’ ={g1, o2, o3, ...}is valid with the same size. - When the argument breaks: If the swap makes the solution worse (e.g., weighted intervals where g1 has lower weight), greedy fails for that problem.
- Greedy fails. This is Weighted Job Scheduling requiring DP:
dp[i] = max(dp[i-1], weight[i] + dp[last_compatible[i]])with binary search. Time: O(n log n).
The Fractional Knapsack is greedy but 0/1 Knapsack needs DP. What fundamental property breaks, and can you give a concrete counterexample?
The Fractional Knapsack is greedy but 0/1 Knapsack needs DP. What fundamental property breaks, and can you give a concrete counterexample?
- Fractional: You can take any fraction, so the greedy choice (best value-per-weight ratio) is always safe — you can adjust the fraction without blocking better options.
- 0/1: Taking an entire item is irreversible and may consume capacity that a combination of smaller items would use more efficiently.
- Counterexample: Capacity = 10. Items: A (weight 6, value 6), B (weight 5, value 5), C (weight 5, value 5). Greedy picks A (all ratios 1.0). Remaining capacity: 4. Total: 6. Optimal: B + C = 10. Greedy missed 40% of the optimal value.
- The broken property: The greedy choice property requires that local decisions do not foreclose globally better solutions. Fractional knapsack has it; 0/1 does not.
- Yes. When all items have the same weight or value is proportional to weight. In these degenerate cases, no trade-off exists between ratio and capacity.
In the Task Scheduler problem, explain why the formula (max_freq - 1) * (n + 1) + count_of_max works. What happens when it does not apply?
In the Task Scheduler problem, explain why the formula (max_freq - 1) * (n + 1) + count_of_max works. What happens when it does not apply?
- Visualization: Place the most frequent task (A, appearing max_freq times) into a grid creating max_freq - 1 gaps of size n between consecutive A’s. The formula
(max_freq - 1) * (n + 1)counts the slots for gaps plus A’s. Add count_of_max_freq_tasks for the final row. - Example: A A A B B B, n=2. Grid:
A _ _ | A _ _ | A. Gaps = 2, each size 3. Final row: A and B (count_max = 2). Total = 2*3 + 2 = 8. - When the formula “breaks”: When there are enough distinct tasks to fill all idle slots. The answer becomes simply len(tasks). The actual answer is
max(len(tasks), formula). - Why max() is needed: If you have 26 different tasks each appearing once and n=2, the formula gives a small number, but you need len(tasks) slots since no idle time is required.
- No. The formula assumes uniform cooldown for all tasks. Variable cooldowns require the simulation approach (max-heap + cooldown queue), which generalizes naturally by setting each task’s available_time based on its own cooldown.
LeetCode Interview Q&A
Three full LeetCode-style questions you should be ready for. Each one comes with a strong-answer framework, the full code, senior follow-ups, common wrong answers, and further reading.Q1: Jump Game II (LC 45) -- explain the greedy approach and prove it is optimal.
Q1: Jump Game II (LC 45) -- explain the greedy approach and prove it is optimal.
- State the structure: at each position you can jump up to
nums[i]further. Goal: minimum jumps to reachn - 1. - Greedy idea: treat each “jump” as a BFS layer. Inside the current layer, scan all reachable indices, tracking the farthest reach for the next jump.
- When you hit the right edge of the current layer, increment the jump count and commit the new layer’s right edge.
- Prove optimality with an exchange argument: any solution that jumps “less far” within a layer can be replaced by jumping to the maximum reach without increasing the count.
- State complexity: O(n) time, O(1) space.
nums = [2, 3, 1, 1, 4]:- i=0: farthest = max(0, 0+2) = 2. i == current_end (0), so jumps=1, current_end=2.
- i=1: farthest = max(2, 1+3) = 4.
- i=2: farthest = 4. i == current_end (2), so jumps=2, current_end=4.
- Loop ends (we exclude index 4). Answer: 2.
max(farthest, i + nums[i]) only when nums[i] >= 0. With unrestricted negatives, the problem becomes BFS on a graph (each node has variable in/out-degree), and complexity rises to O(n^2) worst case.dp[i] = min cost to reach i. The example shows that “minimum count” and “minimum weighted sum” require different tools even on the same input shape.- Naive BFS with explicit queue: works but O(n) extra space and slower constants.
- DP
dp[i] = min(dp[j] + 1 for j in 0..i if j + nums[j] >= i): O(n^2), too slow forn = 10^4. - “Always jump to the farthest reachable index” (single greedy step): wrong — you must scan the entire current layer first.
- LC 55 — Jump Game (the boolean prerequisite)
- LC 1306 — Jump Game III (BFS on indices)
- LC 1340 — Jump Game V (DP variant)
Q2: Task Scheduler (LC 621) -- explain the cooldown formula and show when it does not apply.
Q2: Task Scheduler (LC 621) -- explain the cooldown formula and show when it does not apply.
- Identify the bottleneck: the most frequent task
max_freqcannot be packed tighter than everyn + 1slots. - Lay out the most frequent task first:
A _ _ A _ _ Aformax_freq = 3, n = 2. That requires(max_freq - 1) * (n + 1) + 1slots. - Account for ties: if
count_maxtasks tie atmax_freq, the final row holdscount_maxof them, so addcount_max - 1extra (or write the formula as(max_freq - 1) * (n + 1) + count_max). - Edge case: if there are enough distinct tasks to fill all idle slots, the answer is just
len(tasks). Takemax(len(tasks), formula). - Complexity: O(n) for the frequency count; the formula itself is O(1).
tasks = ["A","A","A","B","B","B"], n = 2:- max_freq = 3, count_max = 2.
- formula = (3 - 1) * (2 + 1) + 2 = 8.
- len(tasks) = 6.
- Answer: max(6, 8) = 8. Layout:
A B _ A B _ A B.
n + 1 ticks: pop up to n + 1 tasks, decrement counts, push back the still-positive ones. Time O(total_tasks * log unique_tasks). Useful when you also need the actual schedule, not just the time — the formula gives only the count.available_time = current_time + task_cooldown. Pop the heap each tick, schedule the most frequent ready task, push it into the cooldown queue with its individual cooldown. O(total_tasks * log unique_tasks).- Sorting tasks by frequency and just spreading them out without the
count_maxterm — fails on ties. - Forgetting
max(len(tasks), formula)— fails when many distinct tasks fill the idle slots. - Simulating the heap correctly but adding
+1instead of accumulating idle time properly — subtle bug, hard to spot in interviews.
- LC 358 — Rearrange String k Distance Apart (Task Scheduler with strings)
- LC 767 — Reorganize String (special case n = 1)
- LC 1405 — Longest Happy String (constrained construction)
Q3: Why doesn't greedy work for Coin Change with arbitrary denominations? Walk through the failure and the DP fix.
Q3: Why doesn't greedy work for Coin Change with arbitrary denominations? Walk through the failure and the DP fix.
- State the greedy: always pick the largest coin that fits.
- Construct the counterexample: coins
[1, 3, 4], amount 6. Greedy gives 4+1+1 = 3 coins; optimal is 3+3 = 2 coins. - Explain why greedy fails: picking the largest coin (4) consumes capacity that a combination of smaller coins (3+3) would use more efficiently. The “greedy choice property” does not hold.
- Prove DP works:
dp[a] = min(dp[a - c] + 1)over all coinscat mosta. Optimal substructure: the best way to makeaends in some coinc, after which you need the best way to makea - c. - Complexity: O(amount * number_of_coins) time, O(amount) space.
coins = [1, 3, 4], amount = 6: dp[0]=0, dp[1]=1, dp[2]=2, dp[3]=1, dp[4]=1, dp[5]=2, dp[6]=2. Answer: 2.[1, 5, 10, 25] are canonical, as are most national currencies. The Pearson-Zhu condition gives an O(n^3) check: greedy is optimal iff for every counterexample candidate amount up to a certain bound, greedy matches DP. In interviews, just say “US coins are canonical so greedy works there, but the general problem is DP.”parent[a] = c table alongside dp[a], recording which coin was chosen to achieve dp[a]. To reconstruct: start at amount, repeatedly subtract parent[a] and prepend it to the answer until a == 0.- Insisting greedy works “with the right sort order.” It does not — the issue is structural, not about ordering.
- Confusing LC 322 (minimum coins) with LC 518 (number of ways). Different DP recurrences (and LC 518 is the one where loop order matters: coins outer, amount inner, otherwise you count permutations instead of combinations).
- Top-down without memoization: exponential and times out.
- LC 518 — Coin Change II (counting variant)
- LC 983 — Minimum Cost For Tickets (similar DP shape)
- LC 322 — Coin Change (this problem)
Q4: Non-overlapping Intervals (LC 435) -- prove sort-by-end is optimal with the exchange argument.
Q4: Non-overlapping Intervals (LC 435) -- prove sort-by-end is optimal with the exchange argument.
- Goal: remove the minimum number of intervals so that the rest are non-overlapping. Equivalently: keep the maximum number of compatible intervals.
- Greedy: sort by end time, sweep left to right, keep an interval if its start is at least the previous kept end.
- Exchange argument: let
gbe greedy’s first kept interval (earliest end),o1the first kept interval in any optimal solution. Sinceg.end <= o1.end, swappingo1forgdoes not break compatibility with the remaining optimal picks. Repeat for every position. - Complexity: O(n log n) for the sort, O(n) sweep.
[[1,2],[2,3],[3,4],[1,3]]: sort by end -> [[1,2],[2,3],[1,3],[3,4]]. Keep [1,2] (end=2). [2,3] starts at 2, keep (end=3). [1,3] starts at 1, skip. [3,4] starts at 3, keep (end=4). keep = 3. Answer: 4 - 3 = 1.dp[i] = max(dp[i-1], weight[i] + dp[p(i)]) where p(i) is the index of the latest interval that does not overlap with i. Find p(i) in O(log n) with binary search. Total O(n log n).[1,2] and [2,3] as non-overlapping (boundary touching is fine). LC 452 treats them as overlapping (the same balloon edge can be hit by one arrow). Adjust the comparison: start >= last_end for non-overlapping, start > last_end for strict separation.- Sorting by start time — a long interval starting early forecloses many short ones. Counterexample:
[[1, 100], [2, 3], [3, 4]]. Sort-by-start keeps[1, 100]and removes the other two. Sort-by-end keeps[2, 3], [3, 4]and removes[1, 100]. - Greedy “remove the longest interval at each step” — O(n^2 log n) and still wrong on edge cases.
- DP from scratch — correct but unnecessarily complicated when greedy works in O(n log n).
- LC 452 — Minimum Number of Arrows to Burst Balloons
- LC 56 — Merge Intervals
- LC 253 — Meeting Rooms II (interval partitioning, different greedy)