name: Greedy
slug: greedy
difficulty_level: 3

description: >
  Make locally optimal choices at each step, hoping to find a global optimum.
  Greedy algorithms are simple and efficient but only work when the problem
  has the greedy choice property—local optima lead to global optimum.

when_to_use: |
  - Interval scheduling (activity selection)
  - Huffman coding
  - Minimum spanning tree (Prim's, Kruskal's)
  - Shortest path with non-negative weights (Dijkstra's)
  - Fractional knapsack

metaphor: |
  Imagine eating at a buffet where you can only fill your plate once. The greedy
  strategy: always take the food that looks most appealing right now. This works
  if what looks best now is actually best overall—but fails if you fill up on
  appetizers and miss the main course.

  Another analogy: making change with the fewest coins. For US currency, always
  using the largest coin that fits (quarter before dime before nickel) gives
  optimal results. But with coins [1, 3, 4], making 6 cents: greedy gives
  4+1+1=3 coins, while optimal is 3+3=2 coins.

core_concept: |
  Greedy algorithms work by making the choice that seems best **at each step**
  without reconsidering previous choices. This works when:

  1. **Greedy choice property**: A locally optimal choice is part of some
     globally optimal solution.

  2. **Optimal substructure**: After making a greedy choice, the remaining
     subproblem has the same structure as the original.

  The key insight is recognizing when greedy works. Common patterns:
  - **Sort by deadline/end time** for scheduling problems
  - **Sort by ratio** (value/weight) for selection problems
  - **Always pick the nearest/smallest/largest** when monotonicity guarantees optimality

  When greedy doesn't work (like 0/1 Knapsack), use dynamic programming instead.

visualization: |
  **Activity Selection (maximize non-overlapping activities):**

  ```
  Activities: [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11)]
  Sort by end time: [(1,4), (3,5), (0,6), (5,7), (3,9), (5,9), (6,10), (8,11)]

  Greedy selection:
  - (1,4): Select ✓ (first activity)
  - (3,5): Skip ✗ (overlaps with (1,4))
  - (0,6): Skip ✗ (overlaps)
  - (5,7): Select ✓ (starts after 4)
  - (3,9): Skip ✗ (overlaps)
  - (5,9): Skip ✗ (overlaps)
  - (6,10): Skip ✗ (overlaps with (5,7))
  - (8,11): Select ✓ (starts after 7)

  Selected: [(1,4), (5,7), (8,11)] — 3 activities
  ```

  **Why sort by end time?**

  ```
  Intuition: Finishing early leaves maximum room for future activities.

  If we picked an activity ending later but overlapping with one ending earlier:
  - We'd block the same activities (both overlap with them)
  - But we'd also potentially block more future activities
  - So the earlier-ending activity is never worse
  ```

  **Jump Game (can reach end?):**

  ```
  Array: [2, 3, 1, 1, 4]

  Greedy: Track farthest reachable position

  i=0: farthest = max(0, 0+2) = 2
  i=1: farthest = max(2, 1+3) = 4  ← can reach end!
  i=2: farthest = max(4, 2+1) = 4
  i=3: farthest = max(4, 3+1) = 4
  i=4: reached end ✓
  ```

code_template: |
  def activity_selection(activities: list[tuple[int, int]]) -> list[tuple]:
      """Select maximum non-overlapping activities."""
      # Sort by end time
      activities.sort(key=lambda x: x[1])

      result = [activities[0]]
      last_end = activities[0][1]

      for start, end in activities[1:]:
          if start >= last_end:  # No overlap
              result.append((start, end))
              last_end = end

      return result


  def can_jump(nums: list[int]) -> bool:
      """Check if you can reach the last index."""
      farthest = 0

      for i in range(len(nums)):
          if i > farthest:
              return False  # Can't reach this position
          farthest = max(farthest, i + nums[i])

      return True


  def min_jumps(nums: list[int]) -> int:
      """Minimum jumps to reach the last index."""
      if len(nums) <= 1:
          return 0

      jumps = 0
      current_end = 0
      farthest = 0

      for i in range(len(nums) - 1):
          farthest = max(farthest, i + nums[i])

          if i == current_end:
              jumps += 1
              current_end = farthest

      return jumps


  def fractional_knapsack(capacity: int,
                          items: list[tuple[int, int]]) -> float:
      """Maximum value with fractional items. Items are (value, weight)."""
      # Sort by value-to-weight ratio (descending)
      items.sort(key=lambda x: x[0] / x[1], reverse=True)

      total_value = 0

      for value, weight in items:
          if capacity >= weight:
              total_value += value
              capacity -= weight
          else:
              # Take fraction of this item
              total_value += value * (capacity / weight)
              break

      return total_value


  def min_meeting_rooms(intervals: list[list[int]]) -> int:
      """Minimum meeting rooms needed."""
      events = []
      for start, end in intervals:
          events.append((start, 1))   # Start event
          events.append((end, -1))    # End event

      events.sort()

      rooms = 0
      max_rooms = 0

      for _, delta in events:
          rooms += delta
          max_rooms = max(max_rooms, rooms)

      return max_rooms


  def partition_labels(s: str) -> list[int]:
      """Partition string so each letter appears in at most one part."""
      last = {c: i for i, c in enumerate(s)}

      partitions = []
      start = end = 0

      for i, c in enumerate(s):
          end = max(end, last[c])  # Extend partition to include all of char c

          if i == end:  # Reached end of partition
              partitions.append(end - start + 1)
              start = i + 1

      return partitions


  def gas_station(gas: list[int], cost: list[int]) -> int:
      """Find starting station to complete circuit."""
      total_surplus = 0
      current_surplus = 0
      start = 0

      for i in range(len(gas)):
          total_surplus += gas[i] - cost[i]
          current_surplus += gas[i] - cost[i]

          if current_surplus < 0:
              # Can't reach next station from current start
              start = i + 1
              current_surplus = 0

      return start if total_surplus >= 0 else -1

recognition_signals:
  - "maximum/minimum number"
  - "interval scheduling"
  - "activity selection"
  - "jump game"
  - "gas station"
  - "partition"
  - "assign"
  - "optimal"
  - "greedy"
  - "earliest/latest"
  - "most/least"

common_mistakes:
  - title: Applying greedy when it doesn't work
    description: |
      Not all optimization problems have the greedy choice property. Using
      greedy on 0/1 Knapsack or Coin Change (with arbitrary coins) gives
      suboptimal results.
    fix: |
      Verify greedy works by proving the greedy choice property, or test
      against known cases. When in doubt, use dynamic programming.

  - title: Wrong sorting criteria
    description: |
      Sorting by the wrong attribute (e.g., start time instead of end time
      for activity selection) leads to suboptimal selections.
    fix: |
      Think about what greedy property you're exploiting. For "maximize
      activities," ending early maximizes remaining time. For "minimize
      lateness," sorting by deadline helps.

  - title: Not handling edge cases
    description: |
      Empty input, single element, or already-solved cases often need
      special handling.
    fix: |
      Check for edge cases before main algorithm:
      ```python
      if not items:
          return 0
      if len(items) == 1:
          return items[0]
      ```

  - title: Greedy from wrong direction
    description: |
      Sometimes greedy works forward but not backward (or vice versa).
      Processing in the wrong order gives wrong results.
    fix: |
      Consider both directions. For interval problems, usually sort by end
      time and process forward. For some problems, working backward reveals
      the greedy choice more clearly.

variations:
  - name: Activity/interval selection
    description: |
      Select maximum non-overlapping intervals. Sort by end time, greedily
      select if no overlap with previous.
    example: "Activity Selection, Non-overlapping Intervals"

  - name: Jump/reach problems
    description: |
      Track farthest reachable position, greedily extend reach.
    example: "Jump Game, Jump Game II, Video Stitching"

  - name: Assignment problems
    description: |
      Match items greedily based on some criteria (smallest to smallest,
      largest to largest, etc.).
    example: "Assign Cookies, Boats to Save People"

  - name: Scheduling
    description: |
      Schedule tasks to minimize lateness or maximize throughput. Often
      involves sorting by deadline or duration.
    example: "Task Scheduler, Meeting Rooms, Car Pooling"

  - name: Huffman coding
    description: |
      Greedily merge two lowest-frequency nodes to build optimal prefix-free
      encoding tree.
    example: "Huffman Coding (not on LeetCode, but classic)"

related_patterns:
  - dynamic-programming
  - intervals

prerequisite_patterns: []
