Skip to the chapter
←DSAGGroundworkΣDSA
DSA/Intermediate/I13
25 / 48 written chapters
/

Levels and plan
  1. B1Complexity analysis
  2. B2JavaScript toolkit
  3. B3Arrays & strings
  4. B4Prefix sums
  5. B5Hashing
  6. B6Two pointers
  7. B7Sliding window
  8. B8Binary search
  9. B9Basic recursion
  10. B10Sorting algorithms
  11. B11Stacks & queues
  12. B12Linked lists
  1. I1Trees
  2. I2BST operations
  3. I3Tree problems in depth
  4. I4Heaps & priority queues
  5. I5Graphs: representation
  6. I6BFS on grids
  7. I7Graph problems
  8. I8Topological patterns
  9. I9Backtracking
  10. I10DP: 1D
  11. I11DP: 2D
  12. I12DP state machines
  13. I13Greedy algorithms
  14. I14Intervals
  15. I15Bit manipulation
  16. I16Matrix problems
  17. I17Tries
  18. I18Monotonic stack & queue
  19. I19Math for interviews
  20. I20Design & simulation
  21. I21Divide and conquer
  22. I22Randomization
  1. A1Advanced DP
  2. A2Union-Find
  3. A3Advanced graph algorithms
  4. A4Minimum Spanning Tree
  5. A5Advanced graphs
  6. A6Segment & Fenwick trees
  7. A7Sparse tables
  8. A8String algorithms
  9. A9Design problems
  10. A10Advanced backtracking
  11. A11Games & probability DP
  12. A12Ordered structures
  13. A13Sweep line & geometry
  14. A14Interview strategy

ΣI13 · Part 2 · Intermediate

Greedy algorithms

The best DP alternative — when the locally best choice happens to be globally best too.

33 min read1 diagram20 sections16 exercises
25 of 48

In this chapter

  • The trade greedy makes
  • The property that has to hold: the greedy-choice property
  • Worked example: Activity/Interval scheduling
  • Proving the rule: the exchange argument and staying ahead
  • Hunting for a counterexample before you trust a rule

and 15 more

about 33 min
20On this page
  1. The trade greedy makes
  2. The property that has to hold: the greedy-choice property
  3. Worked example: Activity/Interval scheduling
  4. Proving the rule: the exchange argument and staying ahead
  5. Hunting for a counterexample before you trust a rule
  6. Worked example: Jump Game — can you reach the end?
  7. Jump Game II: the fewest jumps, one boundary at a time
  8. Worked example: Gas Station
  9. Candy: two passes, one constraint each
  10. Partition Labels: each letter's last occurrence sets the cut
  11. Sort, then match from the right end: Assign Cookies and Boats
  12. Queue Reconstruction by Height: place the tallest first
  13. Minimum Arrows to Burst Balloons: the same shape as interval scheduling
  14. Greedy on counts: Task Scheduler and Reorganize String
  15. Valid Parenthesis String: track the range of open brackets
  16. Best Time to Buy and Sell Stock II: add up every rise
  17. Where greedy is exactly right: fractional knapsack and Huffman coding
  18. Where greedy fails: three counterexamples to keep in your head
  19. Greedy vs DP — how to decide which one a problem wants
  20. Recognizing it in an unseen problem

The trade greedy makes

DP explores every relevant option and keeps whichever turns out best. Greedy skips that entirely: at each step, take whatever looks best right now, commit to it, and never reconsider. That's a huge shortcut when it works — usually O(n log n) instead of DP's O(n²) or worse — but it only produces the correct answer for problems with a specific mathematical property.

The property that has to hold: the greedy-choice property

A problem is safe for greedy only if a locally optimal choice is always part of some globally optimal solution — choosing it never closes off the best overall answer. If that's not provably true, greedy will find a valid answer, just not always the best one, and it will fail silently — no error, just a wrong result on some input you didn't test.

⚠ Greedy's biggest risk: it "works" on your test cases and fails in the interview follow-up The interviewer's next question is almost always "can you prove that's optimal?" or a counter-example that breaks it. If you can't argue why the greedy choice is always safe, say so explicitly and fall back to DP — guessing greedy without justification is a bigger red flag than just using DP from the start.

Worked example: Activity/Interval scheduling

Maximize the number of non-overlapping intervals you can select. The greedy choice: always take the interval that finishes earliest among the remaining valid options.

A (picked) B (conflicts with A) C (picked) D (conflicts with C) E (picked) F (picked)
Sort by finish time, always take the next interval that starts after the last one you picked ends.
function maxNonOverlapping(intervals) {
  const sorted = [...intervals].sort((a, b) => a[1] - b[1]); // sort a copy by FINISH time — the entire trick

  let count = 0, lastEnd = -Infinity;
  for (const [start, end] of sorted) {
    if (start >= lastEnd) { // this one doesn't conflict with our last pick
      count++;
      lastEnd = end;
    }
  }
  return count;
}

Why finish time and not start time or duration: picking whatever finishes earliest leaves the maximum possible room for everything that comes after — any other choice can only leave equal or less room. That's the actual proof sketch, and being able to say it is what separates "I memorized this" from "I understand why it's safe."

Proving the rule: the exchange argument and staying ahead

The sketch above, "finishing earliest leaves the most room", is the idea of a proof. When an interviewer asks "why is that optimal?", you want it in three or four sentences. Two shapes of argument cover almost every greedy rule you will meet, and it helps to see both on the same problem.

The exchange argument: swap in the greedy choice

Take any best schedule and call it O, with its intervals in time order. Let g be the interval the greedy rule picks first, the one that finishes earliest of all. If O already starts with g, there is nothing to show. If not, O starts with some interval o1, and g finishes no later than o1 does, because g finishes earliest of all. Everything else in O starts at or after the end of o1, so it also starts at or after the end of g. Replace o1 with g: no overlap appears, and the count is unchanged. So some best schedule contains the greedy first choice. Now throw away g and every interval that overlaps it. What is left of O is still a valid schedule for the intervals that remain, so the same argument applies to the smaller problem, and repeating it shows the greedy schedule is a best one.

The pattern to copy is: pick an arbitrary best solution, change it so that it agrees with the greedy choice, and show it did not get worse. It suits rules that decide "what goes first" or "who gets matched with whom", and it is the argument behind the cookie, boat and queue problems later in this chapter.

Staying ahead: greedy is never behind

The second shape compares the greedy answer with any other answer step by step and shows it is never behind. For scheduling, let greedy pick g1, g2, ... and let any valid schedule pick o1, o2, ..., both in time order. Claim: the end of gi is never later than the end of oi. For i = 1 this is the rule itself. If it holds for i, then oi+1 starts after oi ends, which is not before gi ends, so oi+1 was available when greedy chose its next interval. Greedy picks the earliest finish among the available ones, so gi+1 ends no later than oi+1. If the other schedule had more intervals than greedy, its extra interval would start after greedy's last end and greedy would not have stopped, a contradiction. So greedy has at least as many.

This one suits a process that moves forward and is summarised by a single number: a finish time, the farthest index reached, the fuel left. Jump Game is the same proof in miniature, because after each step the greedy "farthest reachable" is at least what any other strategy has reached.

Say it like this → "Take any optimal schedule. Its first interval ends no earlier than the one I pick, so I can swap mine in without creating an overlap, and the count stays the same. So some optimal schedule starts with my choice, and I repeat that on what is left."

Hunting for a counterexample before you trust a rule

A proof takes minutes to write, and a counterexample takes seconds to find if you let the machine look. Keep a slow, obviously correct brute force next to the greedy rule, feed both random small inputs, and stop at the first disagreement. Small inputs matter: counterexamples to greedy rules usually need only three to five items, and a narrow range of values forces the ties and touching endpoints where rules break.

function bruteMaxNonOverlapping(intervals) {
  const n = intervals.length;
  let best = 0;
  for (let mask = 0; mask < (1 << n); mask++) { // every subset of the intervals
    const chosen = [];
    for (let i = 0; i < n; i++) if (mask & (1 << i)) chosen.push(intervals[i]);
    chosen.sort((a, b) => a[0] - b[0]);
    let ok = true;
    for (let i = 1; i < chosen.length; i++) {
      if (chosen[i][0] < chosen[i - 1][1]) { ok = false; break; } // overlaps its neighbour
    }
    if (ok) best = Math.max(best, chosen.length);
  }
  return best;
}
function findCounterexample(greedy, brute, makeInput, tries = 2000) {
  for (let t = 0; t < tries; t++) {
    const input = makeInput();
    if (greedy(input) !== brute(input)) return input; // the first input where they disagree
  }
  return null;
}

Try it on the tempting wrong key, sorting by start time:

function maxNonOverlappingByStart(intervals) {
  const sorted = [...intervals].sort((a, b) => a[0] - b[0]); // the tempting but wrong sort key
  let count = 0, lastEnd = -Infinity;
  for (const [start, end] of sorted) {
    if (start >= lastEnd) {
      count++;
      lastEnd = end;
    }
  }
  return count;
}

Calling findCounterexample(maxNonOverlappingByStart, bruteMaxNonOverlapping, makeInput) with a generator of five or fewer random intervals finds a failing input within a few dozen tries: in a run of 20,000 random inputs about one in ten disagreed with the brute force. The shape to expect is [[1, 10], [2, 3], [4, 5]]: sorted by start, the long interval comes first and blocks the other two, so the rule returns 1 while the best schedule has 2. The same call with the finish-time rule returns null.

⚠ Passing the stress test is evidence, not a proof A few thousand clean runs mean you have not found a counterexample, not that none exists. It is still the fastest way to kill a wrong rule, and it works in an interview as a sentence: "I would check this against a brute force on small random inputs." The brute force takes exponential time, so it is only for tiny inputs. And if the generator never produces ties or touching endpoints, it will miss the cases that matter.

Worked example: Jump Game — can you reach the end?

function canJump(nums) {
  let farthestReachable = 0;
  for (let i = 0; i < nums.length; i++) {
    if (i > farthestReachable) return false; // stuck — can't even reach index i
    farthestReachable = Math.max(farthestReachable, i + nums[i]);
  }
  return true;
}

The greedy insight: you never need to know which path gets you furthest, only the single number "furthest index reachable so far" — tracking every possible path (which DP would do) is unnecessary work because only the maximum ever matters for future decisions.

Jump Game II: the fewest jumps, one boundary at a time

Now the end is always reachable and you want the fewest jumps to get there. Trying every sequence of jumps is exponential, and a DP over "fewest jumps to each index" is O(n²) because each index looks at all the indices it can jump to. The greedy view is a breadth-first search without the queue. Look at the set of indices reachable with j jumps. It is a block that starts at index 0 and ends at some index currentEnd: if you can land on an index, you can also land on any earlier one with no more jumps, by jumping a bit shorter. The block reachable with j + 1 jumps therefore ends at the farthest reach i + nums[i] of any index i inside the current block.

So scan left to right, keep the farthest reach seen so far, and every time you step past the end of the current block you must spend one more jump, and the new block ends at that farthest reach.

function jump(nums) {
  let jumps = 0, currentEnd = 0, farthest = 0;
  for (let i = 0; i < nums.length - 1; i++) {
    farthest = Math.max(farthest, i + nums[i]);
    if (i === currentEnd) { // the last index reachable with the current number of jumps
      if (farthest <= i) return -1; // nothing beyond i is reachable
      jumps++;
      currentEnd = farthest;
    }
  }
  return jumps;
}

Dry run: jump([2, 3, 1, 1, 4])

inums[i]farthesti === currentEnd?jumpscurrentEnd
022yes: spend a jump12
134no12
214yes: spend a jump24
314no24

The first jump covers indices 1 to 2. Looking inside that block, index 1 reaches farthest (1 + 3 = 4), so the second jump covers the last index, and the answer is 2. You never decide which index to jump from; you only record how far the best one could have taken you. The loop is O(n) time and O(1) space, and the proof is the staying-ahead argument above: after every jump, currentEnd is as far as any strategy with that many jumps could be.

⚠ Stop one short, and decide what unreachable means The loop runs to nums.length - 1, exclusive. If it included the last index, the moment i equals currentEnd there would count one jump too many, because you do not need to jump from the destination. The line if (farthest <= i) return -1 catches an array where you get stuck, such as [1, 0, 1]. The standard problem promises that the end is reachable, and then that line never fires, but a version that quietly returns a wrong number on an unreachable input is worse than one that says so.

Worked example: Gas Station

function canCompleteCircuit(gas, cost) {
  let total = 0, tank = 0, start = 0;
  for (let i = 0; i < gas.length; i++) {
    const diff = gas[i] - cost[i];
    total += diff;
    tank += diff;
    if (tank < 0) {       // can't reach the next station from any point up to here
      start = i + 1;       // so the answer, if any, must start AFTER i
      tank = 0;
    }
  }
  return total >= 0 ? start : -1; // total < 0 means no valid start exists anywhere
}

Dry run: gas = [1, 2, 3, 4, 5], cost = [3, 4, 5, 1, 2]

igas[i]cost[i]difftotaltanktank < 0?
013-2-2-2yes → start = 1, tank = 0
124-2-4-2yes → start = 2, tank = 0
235-2-6-2yes → start = 3, tank = 0
3413-33no
452306no

total ends at 0 (≥ 0, so a valid start exists) and the last reset left start = 3 — station 3 is the answer. Every candidate start before index 3 failed somewhere on the way to it, so none of them could ever have worked either, which is exactly the argument below.

This one's greedy argument is subtler: if the tank goes negative arriving at station i, starting from any station between the current start and i would also fail, because each of those partial sums was non-negative up to the point of failure — so it's always safe to jump the candidate start forward to i + 1 without missing a valid answer.

Candy: two passes, one constraint each

Children stand in a line with ratings. Each gets at least one candy, and a child with a higher rating than a neighbour must get more candy than that neighbour. Find the fewest candies. Each child has two constraints, one against the left neighbour and one against the right, and they pull against each other. Trying to satisfy both in one pass is where people get stuck. Split them: handle the left neighbours in one pass and the right neighbours in another, always giving the smallest number that works.

Left to right, a child rated higher than the one on their left gets one more than that child, and everyone else stays at 1. Now every left constraint holds, and each number is the smallest that makes it so. Then right to left, a child rated higher than the one on their right needs more than that child. But they may already hold a larger number from the first pass, and lowering it would break a left constraint. So the second pass raises the number to the larger of what it has and what the right neighbour requires.

function candy(ratings) {
  const n = ratings.length;
  const give = new Array(n).fill(1); // everyone gets at least one

  for (let i = 1; i < n; i++) {
    if (ratings[i] > ratings[i - 1]) give[i] = give[i - 1] + 1; // beat the left neighbour
  }
  for (let i = n - 2; i >= 0; i--) {
    if (ratings[i] > ratings[i + 1]) give[i] = Math.max(give[i], give[i + 1] + 1); // and the right one
  }
  return give.reduce((sum, x) => sum + x, 0);
}

Dry run: ratings [1, 0, 2]

Stepcandieswhy
start[1, 1, 1]everyone gets one
left to right, i = 1[1, 1, 1]0 is not higher than 1
left to right, i = 2[1, 1, 2]2 is higher than 0, so one more than the 1
right to left, i = 1[1, 1, 2]0 is not higher than 2
right to left, i = 0[2, 1, 2]1 is higher than 0: max(1, 1 + 1) = 2

The total is 5. The second pass shows why the Math.max matters. With ratings [1, 3, 4, 5, 2] the first pass gives [1, 2, 3, 4, 1]. At the peak, 5 is higher than the 2 on its right, which needs only 1 + 1 = 2 candies, but the peak already holds 4 from the climb. Assigning 2 would break the rule with the 4-rated neighbour on its left, so the second pass keeps 4 and the total is 11. Children with equal ratings do not constrain each other: [1, 2, 2] gets [1, 2, 1], which is 4. Time is O(n) and space is O(n) for the array of candies.

Partition Labels: each letter's last occurrence sets the cut

Cut a string into as many parts as possible so that no letter appears in more than one part, and return the size of each part. A letter that appears twice forces everything between its first and last occurrence into one part. Chasing those overlaps is the same merging problem as intervals, but there is a shortcut: you only need the last index of each letter. Walk the string, keep the farthest last-occurrence of any letter you have passed, and the moment the walk reaches that index, every letter in the part ends here, so you can cut.

function partitionLabels(s) {
  const last = new Map();
  for (let i = 0; i < s.length; i++) last.set(s[i], i); // last index of each letter

  const sizes = [];
  let start = 0, end = 0;
  for (let i = 0; i < s.length; i++) {
    end = Math.max(end, last.get(s[i])); // the part must reach this letter's last occurrence
    if (i === end) { // nothing inside the part reaches beyond here: cut
      sizes.push(end - start + 1);
      start = i + 1;
    }
  }
  return sizes;
}

Dry run: partitionLabels("abcabdd")

last occurrences: a = 3, b = 4, c = 2, d = 6

iletterlast[letter]endaction
0a33none, the part must reach index 3
1b44none, the part must now reach index 4
2c24none
3a34none
4b44i === end: cut, size 4 - 0 + 1 = 5, start = 5
5d66none
6d66i === end: cut, size 6 - 5 + 1 = 2

The result is [5, 2]. Cutting at the earliest possible point is safe because any valid partition must put everything up to that end into one part, so a cut anywhere earlier is impossible and a later one only makes the parts fewer. The first loop is O(n) and the second is O(n), with at most one entry per distinct letter in the map. A common slip is to use each letter's first occurrence, or to return the cut positions when the question asks for the sizes.

Sort, then match from the right end: Assign Cookies and Boats

A large family of greedy problems is "sort, then pair things off". The skill is deciding which end to serve first. Two small examples show how the exchange argument picks it.

Assign Cookies

Each child has a greed factor, each cookie has a size, and a child is happy with a cookie at least as big as their greed. Each cookie goes to at most one child. Maximise the number of happy children. Serve the least greedy child first, with the smallest cookie that satisfies them. A cookie too small for the least greedy child is too small for everyone, so it is thrown away. Why is the smallest sufficient cookie a safe choice? In any best plan, if that cookie went to another child, or to nobody, give it to the least greedy child instead. If the least greedy child held a different cookie, that cookie is at least as big as this one, so it still satisfies whoever received this one. The number of happy children does not drop, and the bigger cookies stay available for greedier children.

function findContentChildren(greed, cookies) {
  const children = [...greed].sort((a, b) => a - b);
  const sizes = [...cookies].sort((a, b) => a - b);
  let child = 0;
  for (let cookie = 0; cookie < sizes.length && child < children.length; cookie++) {
    if (sizes[cookie] >= children[child]) child++; // the smallest cookie that satisfies the least greedy child
  }
  return child;
}

With greed [1, 2, 3] and cookies [1, 1], the first cookie satisfies the child with greed 1, and the second is too small for the child with greed 2 and is dropped, so the answer is 1. Time is O(n log n + m log m) for the two sorts, and the matching pass is linear.

Boats to Save People

Each boat carries at most two people and a total weight of at most limit, and every person alone fits. Use the fewest boats. Look at the heaviest person H and the lightest person L. If L and H cannot share a boat, no one can share with H, so H rides alone. If they can, pair them, and the exchange argument says that is safe. Suppose some best plan has H with a partner P and L with a partner Q. Regroup the four as (L, H) and (P, Q). Since L weighs no more than P, L + H is at most P + H. Since Q weighs no more than H, P + Q is at most P + H. Both are within the limit because P + H was, and the number of boats is the same.

function numRescueBoats(people, limit) {
  const weights = [...people].sort((a, b) => a - b);
  let light = 0, heavy = weights.length - 1, boats = 0;
  while (light <= heavy) {
    if (weights[light] + weights[heavy] <= limit) light++; // the lightest rides with the heaviest
    heavy--; // the heaviest always leaves in this boat
    boats++;
  }
  return boats;
}

Dry run: numRescueBoats([3, 2, 2, 1], 3)

sorted weights: [1, 2, 2, 3]

lightheavyweights[light] + weights[heavy]actionboats
031 + 3 = 4 > 3the 3 rides alone1
021 + 2 = 3 ≤ 3the 1 and a 2 share2
112 + 2 = 4 > 3the last 2 rides alone3

The loop condition is light <= heavy, so the final person, when both pointers meet, still gets a boat. Time is O(n log n) for the sort and O(n) for the two-pointer sweep. The code assumes every person's weight is at most limit; if that were not promised, you would first have to say what to do about someone who cannot ride at all.

Queue Reconstruction by Height: place the tallest first

Each person is a pair [h, k]: height h, and exactly k people in front of them who are at least as tall. Rebuild a queue that satisfies everyone. The trick is to build the queue in the order that makes each placement final. When you place a person, consider only taller or equal people: if you have inserted people from tallest to shortest, then everyone already in the queue is at least as tall, so inserting at index k puts exactly k of them in front. People inserted later are shorter, so they never change the count of anyone placed before.

Sort by height descending. Break ties by k ascending, so that among equal heights the one with fewer people in front goes in first and the next one counts it correctly.

function reconstructQueue(people) {
  const sorted = [...people].sort((a, b) => b[0] - a[0] || a[1] - b[1]); // tallest first, fewer-in-front first
  const queue = [];
  for (const person of sorted) {
    queue.splice(person[1], 0, [...person]); // k taller people are already placed: insert at index k
  }
  return queue;
}

Dry run: [[7, 0], [4, 4], [7, 1], [5, 0], [6, 1], [5, 2]]

Insertat indexqueue afterwards
[7, 0]0[7,0]
[7, 1]1[7,0] [7,1]
[6, 1]1[7,0] [6,1] [7,1]
[5, 0]0[5,0] [7,0] [6,1] [7,1]
[5, 2]2[5,0] [7,0] [5,2] [6,1] [7,1]
[4, 4]4[5,0] [7,0] [5,2] [6,1] [4,4] [7,1]

Check the pair [6, 1]: in the final queue, exactly one person before it, the 7, is at least as tall. Each splice shifts the elements after the insertion point, so the worst case is O(n²) for the inserts on top of the O(n log n) sort. If the tie-break were k descending, the person with more people in front would go in first and be shifted out of position by the one inserted after. The code sorts a copy and stores copies of the pairs, so the caller's data is never changed.

Minimum Arrows to Burst Balloons: the same shape as interval scheduling

Each balloon is a horizontal span [xStart, xEnd], and an arrow shot straight up at x bursts every balloon whose span contains x, ends included. Find the fewest arrows. Sort by the right end and shoot the first arrow at the right end of the first balloon: that is as far right as an arrow can go while still hitting it, so it hits as many later balloons as possible. Every balloon whose start is at or before that point is burst. The first balloon that starts after it needs a new arrow, again at its right end.

function findMinArrowShots(points) {
  if (points.length === 0) return 0;
  const sorted = [...points].sort((a, b) => a[1] - b[1]); // by right end
  let arrows = 1, arrowAt = sorted[0][1]; // shoot at the first balloon's right end
  for (let i = 1; i < sorted.length; i++) {
    if (sorted[i][0] > arrowAt) { // starts after the arrow: not hit, so shoot again
      arrows++;
      arrowAt = sorted[i][1];
    }
  }
  return arrows;
}

Dry run: [[10, 16], [2, 8], [1, 6], [7, 12]]

sorted by right end: [1, 6], [2, 8], [7, 12], [10, 16]

Balloonstarts after the arrow?actionarrows
[1, 6]first balloonshoot at 61
[2, 8]2 > 6? noburst by the arrow at 61
[7, 12]7 > 6? yesshoot at 122
[10, 16]10 > 12? noburst by the arrow at 122

The answer is 2. The arrow count equals the largest number of balloons that can be picked so that no two share a point, which is the scheduling problem from earlier with one change: here two spans that only touch, like [1, 2] and [2, 3], share the point 2 and one arrow bursts both, so the test is a strict >, where the scheduling code used >=. The intervals chapter lines up this touching rule for every interval problem side by side. Time is O(n log n).

Greedy on counts: Task Scheduler and Reorganize String

Some greedy problems are about how often each item occurs, and the busiest item sets the shape of the whole answer. Both problems below have a heap solution (always take the most frequent item that is allowed), which the heaps chapter covers. Here is the version that needs no heap, with the reason it works.

Task Scheduler: the busiest task sets the frame

Each task takes one time unit, the CPU may idle, and two runs of the same task must be at least n units apart. Return the fewest units. Let the most frequent task occur m times. Its occurrences need m - 1 gaps, and each gap, together with the run before it, spans n + 1 units: A _ _ A _ _ A for n = 2. The last occurrence closes the frame, along with any other task that is tied for the highest count. So the frame has length (m - 1) × (n + 1) + withMax, where withMax is how many tasks occur exactly m times. No schedule can be shorter. The other tasks drop into the empty slots of the frame. If there are so many of them that they do not fit, the schedule never has to idle at all, and the answer is simply the number of tasks.

function leastInterval(tasks, n) {
  const counts = new Map();
  for (const task of tasks) counts.set(task, (counts.get(task) || 0) + 1);

  let maxCount = 0;
  for (const count of counts.values()) maxCount = Math.max(maxCount, count);
  let withMax = 0;
  for (const count of counts.values()) if (count === maxCount) withMax++;

  const frame = (maxCount - 1) * (n + 1) + withMax; // the busiest task sets the frame
  return Math.max(tasks.length, frame); // enough other tasks fill every idle slot
}

Dry run: AAABBB with n = 2, then AAABBCCDE with n = 2

TasksmwithMaxframetasks.lengthanswer
AAABBB, n = 232(3 - 1) × 3 + 2 = 868
AAABBCCDE, n = 231(3 - 1) × 3 + 1 = 799

The first schedule is A B _ A B _ A B: eight units with two idles. In the second, the other six tasks fill the gaps and spill over, so every unit can be busy, and a valid order exists such as A B C A B C A D E. The code counts with a Map in O(n) time, and only the number of distinct tasks is stored.

Reorganize String: fill the even slots first

Rearrange a string so that no two equal letters are adjacent, or return the empty string if that is impossible. If one letter occurs more than ceil(length / 2) times, no arrangement exists, because that letter would need more slots than there are pairwise non-adjacent positions. Otherwise lay the letters down one letter at a time, most frequent first, into the slots 0, 2, 4, ... and then, when those are full, 1, 3, 5, ...

function reorganizeString(s) {
  const counts = new Map();
  for (const ch of s) counts.set(ch, (counts.get(ch) || 0) + 1);

  const letters = [...counts.keys()].sort((a, b) => counts.get(b) - counts.get(a)); // most frequent first
  if (s.length > 0 && counts.get(letters[0]) > Math.ceil(s.length / 2)) return "";

  const out = new Array(s.length);
  let pos = 0;
  for (const ch of letters) {
    for (let k = 0; k < counts.get(ch); k++) {
      out[pos] = ch;
      pos += 2; // skip a slot so equal letters are never neighbours
      if (pos >= s.length) pos = 1; // evens are full: continue on the odd slots
    }
  }
  return out.join("");
}

Why can a letter never touch itself? Each letter's slots are consecutive in that fill order. A letter that sits entirely in even slots, or entirely in odd slots, has slots two apart. Only a letter that starts on an even slot and runs on into the odd slots could land next to itself, and it would need more occurrences than the letter laid down before it, which the sort order rules out. The first letter cannot wrap, because the check above guarantees it fits in the even slots. For "aaabbc" the fill order gives a at 0, 2, 4, then b at 1 and 3, then c at 5: "ababac". For "aaab" the letter a occurs 3 times, more than ceil(4 / 2) = 2, so the answer is the empty string. Time is O(n + k log k) for k distinct letters.

Valid Parenthesis String: track the range of open brackets

A string holds (, ) and *, where a star may be (, ) or nothing. Is there a way to choose the stars so the brackets balance? Trying every choice is 3k for k stars. Instead of choosing, track every count of open brackets that is still possible. That set is always a block of consecutive integers, so two numbers describe it: lo, the fewest brackets that could be open, and hi, the most. A ( moves both up by one, a ) moves both down by one, and a star widens the block: it could close one (lo - 1), open one (hi + 1) or do nothing.

Two repairs keep the block honest. If hi drops below 0, even treating every star as an opening bracket leaves a ) with no partner, so the string is invalid. And lo is clamped at 0, because a negative count of open brackets is not a possible state; it only means some of the choices are impossible. At the end the string is valid exactly when 0 is inside the block, and since lo was clamped, that means lo === 0.

function checkValidString(s) {
  let lo = 0, hi = 0; // fewest and most "(" that could still be open
  for (const ch of s) {
    if (ch === "(") { lo++; hi++; }
    else if (ch === ")") { lo--; hi--; }
    else { lo--; hi++; } // a star could close one, do nothing, or open one
    if (hi < 0) return false; // even with every star as "(", a ")" has no partner
    if (lo < 0) lo = 0; // "fewest open" cannot go below zero
  }
  return lo === 0; // some choice of stars closes everything
}

Dry run: checkValidString("(*))")

Characterlo, hi before the clamplo, hi after
(1, 11, 1
*0, 20, 2
)-1, 10, 1
)-1, 00, 0

hi never goes below 0 and lo ends at 0, so the string is valid: read the star as ( and you get (()). The string ")*(" fails at the first character, because hi becomes -1 at once. Forgetting the clamp on lo is the usual bug: "*)" is valid (read the star as (), but without the clamp lo ends at -2 and the string is wrongly rejected. One pass, O(n) time, O(1) space.

Best Time to Buy and Sell Stock II: add up every rise

With as many buy-and-sell pairs as you like (holding at most one share at a time), the profit is the sum of every day-to-day rise. Any profitable transaction from day i to day j is the sum of the daily changes in between, and it cannot beat the sum of the positive daily changes alone, because the positive ones are all of the gain and the negative ones only subtract. Buying at the start of every rise and selling at its end collects exactly the positive changes.

function maxProfitMany(prices) {
  let profit = 0;
  for (let i = 1; i < prices.length; i++) {
    if (prices[i] > prices[i - 1]) profit += prices[i] - prices[i - 1]; // collect every rise
  }
  return profit;
}

For [7, 1, 5, 3, 6, 4] the rises are 1 to 5 (4) and 3 to 6 (3), and the profit is 7. The same answer comes from "buy at 1, sell at 5, buy at 3, sell at 6". Time is O(n), space O(1). The moment a rule is added that changes what a transaction costs or when you may start another, this stops being greedy: a cooldown after selling, a fee per transaction, or a cap on the number of transactions all need a small DP over "holding" and "not holding", which is in DP state machines.

Where greedy is exactly right: fractional knapsack and Huffman coding

Fractional knapsack, and why the 0/1 version is different

You have items with a weight and a value, a bag with a weight limit, and you may take part of an item. Take items in order of value per unit of weight, the best ratio first, and split the last one that does not fit whole. The exchange argument is one line: if a plan uses some weight on a lower-ratio item while a higher-ratio item still has unused weight, move that weight across and the value goes up or stays equal.

function fractionalKnapsack(items, capacity) {
  const sorted = [...items].sort((a, b) => b[1] / b[0] - a[1] / a[0]); // best value per unit of weight first
  let value = 0, room = capacity;
  for (const [weight, worth] of sorted) {
    if (room === 0) break;
    const take = Math.min(weight, room); // all of it, or whatever still fits
    value += worth * (take / weight);
    room -= take;
  }
  return value;
}

With items [weight, value] of [10, 60], [20, 100] and [30, 120] and a limit of 50, the ratios are 6, 5 and 4. Take all of the first (60), all of the second (100), and 20 of the 30 units of the third (80): total 240. Weights must be positive, or the ratio divides by zero.

The identical data under the 0/1 rule, where an item is taken whole or not at all, gives a different story. Ratio order takes the first two items, weight 30 and value 160, and the third no longer fits. But the second and third items together weigh exactly 50 and are worth 220. The splitting freedom is what made greedy safe, and without it the problem is the 0/1 knapsack, which needs the table in the 2D DP chapter.

Huffman coding: always merge the two lightest

A prefix code gives each symbol a bit string so that no code is the start of another. Frequent symbols should get short codes. Huffman's rule builds the best code tree from the bottom: repeatedly take the two lightest nodes, merge them under a new node whose weight is their sum, and put that node back. The total cost of the code, the sum over symbols of frequency times code length, equals the sum of all the merged weights, because each merge adds the weight of everything below it one level deeper.

The standard implementation uses a min-heap. There is a heap-free version that is easy to write: sort the frequencies once, and keep the merged sums in a second queue. The sums come out in non-decreasing order, because each merge takes the two smallest items available and the smallest items only grow, so the second queue stays sorted by itself. At each step the two smallest are among the fronts of the two queues.

function huffmanCost(freqs) {
  if (freqs.length < 2) return 0;
  const leaves = [...freqs].sort((a, b) => a - b);
  const merged = []; // sums come out in non-decreasing order, so a plain queue stays sorted
  let i = 0, j = 0, total = 0;

  const takeSmallest = () => {
    if (j >= merged.length || (i < leaves.length && leaves[i] <= merged[j])) return leaves[i++];
    return merged[j++];
  };

  for (let step = 0; step < freqs.length - 1; step++) {
    const sum = takeSmallest() + takeSmallest(); // merge the two lightest
    merged.push(sum);
    total += sum;
  }
  return total;
}

Dry run: huffmanCost([5, 9, 12, 13, 16, 45])

StepTwo lightestNew nodeRunning total
15 and 9 (both leaves)1414
212 and 13 (both leaves)2539
314 (merged) and 16 (leaf)3069
425 and 30 (both merged)55124
545 (leaf) and 55 (merged)100224

The cost is 224. With n symbols there are n - 1 merges. Sorting is O(n log n) and the merging is O(n) after it. The heap version is O(n log n) throughout; it is the same loop as the heap exercise on connecting sticks at the lowest total cost. The proof is again an exchange argument: the two rarest symbols can always be made siblings at the deepest level of some best tree.

Where greedy fails: three counterexamples to keep in your head

Tempting ruleInput that breaks itWhat it returns, and the right answerUse instead
Fewest coins: always take the largest coin that fitscoins 1, 3, 4; amount 64 + 1 + 1, three coins; the best is 3 + 3, two coinsDP over the amount
0/1 knapsack: take items by best value per unit of weightweights 6, 5, 5; values 10, 7, 7; limit 10takes the first item, value 10; the best is the other two, value 14the 2D table
Most intervals: sort by start time and take what fits[1, 10], [2, 3], [4, 5]takes [1, 10] and stops, count 1; the best is 2sort by end time

All three rules sound reasonable, all three work on many inputs, and each fails on an input small enough to write on a whiteboard. Keep these three in mind as a reflex: before you commit to a greedy rule, spend a minute looking for a small input where it fails, which is what the stress test above automates.

Greedy vs DP — how to decide which one a problem wants

SignalPoints toward
"Maximum/minimum number of X" with a simple, provable local ruleGreedy
You can sort by one property and process in that orderGreedy
The best choice now can make a later choice worse in a way you can't undoDP
You keep wanting to say "but what if I hadn't picked that one"DP — that's the tell you need to explore alternatives
Say it like this → "I'll try the greedy approach — sort by finish time and always take the next non-conflicting option — and I can justify it because taking the earliest-finishing option never leaves less room than any other choice would, so it can't cost us a better solution."

Recognizing it in an unseen problem

  • "Minimum number of," "maximum number of," where a sorted, greedy-order decision seems natural
  • You can articulate why the greedy choice never eliminates the optimal answer — if you can't, don't trust it
  • Scheduling, interval, and "assign resources" problems are greedy's home turf
  • "Sort, then pair the ends": cookies to children, people to boats, anything where the extreme item should take the partner that costs the least
  • One number summarises progress (farthest reach, finish time, fuel, open brackets) → a single pass that keeps only that number, often with a range instead of a choice
  • Counts decide the answer (the busiest task, the most frequent letter) → build the frame the busiest item forces, then fit the rest into it
  • A rule that sounds right but you cannot prove → test it against a brute force on small random inputs, and keep the three counterexamples above in mind
  • Changing the problem slightly (fractions allowed, a cooldown, a fee, a cap on transactions) can flip it between greedy and DP, so re-check the property each time
  • When in doubt in an interview: try to prove greedy for a minute; if you can't, say so and switch to DP rather than silently guessing
Before you move on
  • State the greedy-choice property in your own words, and say what "fails silently" means for an unproven greedy approach.
  • Explain why sorting interval scheduling by finish time (not start time, not duration) is the part that makes it correct.
  • Write Jump Game and say why tracking a single "farthest reachable" number is enough, without exploring every path.
  • Trace Gas Station on a small example and explain why a failed start can jump straight past every station up to the failure point.
  • Given a new problem, try to argue why the greedy choice is safe for a minute — and say out loud when you can't, rather than guessing.
  • Write the exchange argument for earliest-finish scheduling in four sentences, and the staying-ahead argument for the same problem.
  • Write a brute force and a random tester that would find a counterexample to a greedy rule, and say what a clean run does and does not show.
  • Write Jump Game II with its boundary variable, and say why the loop stops before the last index.
  • Explain why Candy needs two passes and why the second one takes a maximum.
  • Explain why Queue Reconstruction inserts the tallest first, and what breaks if ties are sorted the other way.
  • Say why Minimum Arrows uses a strict comparison where interval scheduling uses an inclusive one.
  • Give the counterexample for coin change with the coins 1, 3 and 4, and for the 0/1 knapsack taken by value per weight.

16Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 8 testsGas Stationintermediate
  • 8 testsJump Gameintermediate
  • 8 testsJump Game IIintermediate
  • 8 testsCandyadvanced
  • 8 testsPartition Labelsintermediate
  • 8 testsAssign Cookiesbeginner
  • 8 testsBoats to Save Peopleintermediate
  • 8 testsQueue Reconstruction by Heightintermediate
  • 8 testsMinimum Number of Arrows to Burst Balloonsintermediate
  • 8 testsValid Parenthesis Stringintermediate
  • 7 testsBest Time to Buy and Sell Stock IIbeginner
  • 8 testsFractional Knapsackintermediate
  • 8 testsNon-overlapping Intervalsintermediate
  • 8 testsTask Schedulerintermediate
  • 8 testsReorganize Stringintermediate
  • 8 testsMinimum Cost to Connect Sticksintermediate
All 318 for this level →

✓Check yourself

5 questions, pass at 4. The check runs in your browser and the result stays on this device, so it is a self-check and not a certificate.

I13

Finished reading?

0 of 48 chapters read in this series

Mark as read

→Next stop

  1. I12Previous · I12DP state machines and the knapsack family
  2. I13you are here
  3. Next · I14IntervalsEvery interval question starts the same way: sort by start time. Then it's bookkeeping.23 min read

01On this page

20 sections · 0%

  1. 01The trade greedy makes
  2. 02The property that has to hold: the greedy-choice property
  3. 03Worked example: Activity/Interval scheduling
  4. 04Proving the rule: the exchange argument and staying ahead
  5. 05Hunting for a counterexample before you trust a rule
  6. 06Worked example: Jump Game — can you reach the end?
  7. 07Jump Game II: the fewest jumps, one boundary at a time
  8. 08Worked example: Gas Station
  9. 09Candy: two passes, one constraint each
  10. 10Partition Labels: each letter's last occurrence sets the cut
  11. 11Sort, then match from the right end: Assign Cookies and Boats
  12. 12Queue Reconstruction by Height: place the tallest first
  13. 13Minimum Arrows to Burst Balloons: the same shape as interval scheduling
  14. 14Greedy on counts: Task Scheduler and Reorganize String
  15. 15Valid Parenthesis String: track the range of open brackets
  16. 16Best Time to Buy and Sell Stock II: add up every rise
  17. 17Where greedy is exactly right: fractional knapsack and Huffman coding
  18. 18Where greedy fails: three counterexamples to keep in your head
  19. 19Greedy vs DP — how to decide which one a problem wants
  20. 20Recognizing it in an unseen problem
Mark as read

[] chapters · t top