title: Boats to Save People
slug: boats-to-save-people
difficulty: medium
leetcode_id: 881
leetcode_url: https://leetcode.com/problems/boats-to-save-people/
categories:
  - arrays
  - two-pointers
  - sorting
patterns:
  - two-pointers
  - greedy

description: |
  You are given an array `people` where `people[i]` is the weight of the i<sup>th</sup> person, and an **infinite number of boats** where each boat can carry a maximum weight of `limit`. Each boat carries **at most two people** at the same time, provided the sum of the weight of those people is at most `limit`.

  Return *the minimum number of boats to carry every given person*.

constraints: |
  - `1 <= people.length <= 5 * 10^4`
  - `1 <= people[i] <= limit <= 3 * 10^4`

examples:
  - input: "people = [1,2], limit = 3"
    output: "1"
    explanation: "1 boat carries both people (1 + 2 = 3 <= limit)."
  - input: "people = [3,2,2,1], limit = 3"
    output: "3"
    explanation: "3 boats needed: (1, 2), (2), and (3). The heaviest person (3) must go alone."
  - input: "people = [3,5,3,4], limit = 5"
    output: "4"
    explanation: "4 boats needed: (3), (3), (4), (5). No two people can share a boat."

explanation:
  intuition: |
    Imagine you're organising an evacuation with limited boats. Each boat can hold at most two people, and there's a weight limit. Your goal is to minimise the number of boats used.

    The key insight is that **heavy people are harder to pair**. A person weighing close to the limit can only share a boat with someone very light — or must go alone. Meanwhile, light people are flexible and can potentially pair with anyone.

    This suggests a **greedy strategy**: try to pair the heaviest remaining person with the lightest remaining person. If they fit together, great — you've efficiently used one boat for two people. If they don't fit, the heavy person must travel alone, and you move on.

    To efficiently find the heaviest and lightest unpaired people, **sort the array first**. Then use two pointers: one at the start (lightest) and one at the end (heaviest). This lets us make pairing decisions in O(1) time per boat.

  approach: |
    We solve this using a **Two Pointers Greedy Approach**:

    **Step 1: Sort the people by weight**

    - Sorting allows us to efficiently consider the lightest and heaviest people
    - After sorting, `people[0]` is lightest, `people[n-1]` is heaviest

    &nbsp;

    **Step 2: Initialise two pointers and a boat counter**

    - `left`: Points to the lightest unpaired person (index `0`)
    - `right`: Points to the heaviest unpaired person (index `n - 1`)
    - `boats`: Counter starting at `0`

    &nbsp;

    **Step 3: Greedily assign boats**

    - While `left <= right` (there are unpaired people):
      - The heaviest person (`people[right]`) **always** needs a boat this round
      - Check if the lightest person can join: `people[left] + people[right] <= limit`
      - If yes: both board together, move `left` right (`left += 1`)
      - Either way: the heavy person is seated, move `right` left (`right -= 1`)
      - Increment `boats` by 1

    &nbsp;

    **Step 4: Return the result**

    - Return `boats` — the total number of boats used

    &nbsp;

    This greedy approach is optimal because pairing the heaviest with the lightest maximises the chance of fitting two people per boat. If the lightest can't pair with the heaviest, no one else can either (since everyone else is heavier).

  common_pitfalls:
    - title: Forgetting to Sort
      description: |
        The two-pointer technique requires the array to be sorted. Without sorting, you can't reliably find the lightest and heaviest unpaired people using pointers.

        For example, with `people = [3,1,2]` and `limit = 3`, an unsorted approach might try pairing `3` with `1`, then `2` alone — but the optimal pairing is `(1,2)` and `(3)`, which requires seeing the sorted order.
      wrong_approach: "Using two pointers on unsorted array"
      correct_approach: "Sort first, then use two pointers"

    - title: Trying to Fit More Than Two People
      description: |
        The problem explicitly states each boat carries **at most two people**. Some solvers try to pack as many light people as possible into one boat.

        Even if `people = [1,1,1]` and `limit = 10`, you still need 2 boats: `(1,1)` and `(1)`. The weight limit isn't the only constraint — the capacity of 2 people is.
      wrong_approach: "Fitting three or more people per boat"
      correct_approach: "Maximum two people per boat, regardless of weight"

    - title: Not Moving the Right Pointer
      description: |
        The heaviest person (`people[right]`) always boards a boat each iteration — whether alone or paired. Forgetting to decrement `right` causes an infinite loop.

        Every iteration must move at least `right` leftward. If a pairing occurs, `left` also moves.
      wrong_approach: "Only moving left when pairing succeeds"
      correct_approach: "Always decrement right; conditionally increment left"

    - title: Using Greedy Without Proof
      description: |
        While greedy works here, it's worth understanding **why**. If the lightest person can't pair with the heaviest, then no one can pair with the heaviest (everyone else is heavier). So the heaviest must go alone — no better option exists.

        This local optimal choice (pair heaviest with lightest if possible) leads to a global optimum.
      wrong_approach: "Assuming greedy works without reasoning"
      correct_approach: "Understanding why pairing extremes is optimal"

  key_takeaways:
    - "**Sort to enable two-pointer greedy**: Sorting transforms the problem into one where extreme elements (lightest/heaviest) are easily accessible"
    - "**Greedy pairing strategy**: Pair the heaviest with the lightest — if they can't fit, the heaviest goes alone"
    - "**Two-pointer efficiency**: After sorting, each person is processed exactly once, giving O(n) for the pairing phase"
    - "**Constraint awareness**: The two-person limit per boat is crucial — this isn't a pure bin-packing problem"

  time_complexity: "O(n log n). Sorting dominates at O(n log n), followed by O(n) for the two-pointer traversal."
  space_complexity: "O(log n) to O(n). Depends on the sorting algorithm's space usage. The two-pointer logic itself uses O(1) extra space."

solutions:
  - approach_name: Two Pointers (Greedy)
    is_optimal: true
    code: |
      def num_rescue_boats(people: list[int], limit: int) -> int:
          # Sort to enable greedy pairing
          people.sort()

          # Two pointers: lightest and heaviest unpaired people
          left, right = 0, len(people) - 1
          boats = 0

          while left <= right:
              # Check if lightest can pair with heaviest
              if people[left] + people[right] <= limit:
                  # Both can share a boat
                  left += 1

              # Heaviest person always boards (alone or paired)
              right -= 1
              boats += 1

          return boats
    explanation: |
      **Time Complexity:** O(n log n) — Dominated by sorting.

      **Space Complexity:** O(log n) to O(n) — Sorting space overhead.

      After sorting, we use two pointers to greedily pair people. The heaviest person always boards each iteration. If the lightest can join them (combined weight <= limit), we advance the left pointer too. This ensures minimum boats because if the lightest can't pair with the heaviest, no one can.

  - approach_name: Counting Sort Optimisation
    is_optimal: false
    code: |
      def num_rescue_boats(people: list[int], limit: int) -> int:
          # Count frequency of each weight
          count = [0] * (limit + 1)
          for weight in people:
              count[weight] += 1

          # Two pointers on weight values
          left, right = 1, limit
          boats = 0

          while left <= right:
              # Skip weights with no people
              while left <= right and count[left] == 0:
                  left += 1
              while left <= right and count[right] == 0:
                  right -= 1

              if left > right:
                  break

              # Try to pair lightest with heaviest
              if left + right <= limit:
                  # Can pair: use one of each
                  count[left] -= 1
                  count[right] -= 1
                  if left == right and count[left] == 1:
                      # Only one person left at this weight
                      count[left] -= 1
                  boats += 1
              else:
                  # Can't pair: heaviest goes alone
                  count[right] -= 1
                  boats += 1

          return boats
    explanation: |
      **Time Complexity:** O(n + limit) — Linear in input size plus weight range.

      **Space Complexity:** O(limit) — Array to count each weight.

      When the weight range is small relative to the number of people, counting sort can be faster than comparison-based sorting. We count frequencies and use two pointers on weight values rather than indices. This approach is more complex and only beneficial when `limit` is small.

  - approach_name: Brute Force (Backtracking)
    is_optimal: false
    code: |
      def num_rescue_boats(people: list[int], limit: int) -> int:
          n = len(people)
          min_boats = [n]  # Worst case: everyone gets their own boat

          def backtrack(index: int, boats: int, current_boat: int):
              # Pruning: if we've already found a better solution, stop
              if boats >= min_boats[0]:
                  return

              # All people assigned
              if index == n:
                  min_boats[0] = min(min_boats[0], boats)
                  return

              weight = people[index]

              # Option 1: Start a new boat with this person
              backtrack(index + 1, boats + 1, weight)

              # Option 2: Add to current boat if possible and space for 2
              if current_boat > 0 and current_boat + weight <= limit:
                  # This simplified version doesn't track boat occupancy properly
                  # Full implementation would need more state
                  pass

          backtrack(0, 0, 0)
          return min_boats[0]
    explanation: |
      **Time Complexity:** O(2^n) — Exponential in the worst case.

      **Space Complexity:** O(n) — Recursion stack depth.

      This brute force approach tries all possible assignments of people to boats. While correct in principle, it's far too slow for the given constraints (`n <= 5 * 10^4`). Included to illustrate why a greedy approach is necessary — exhaustive search is infeasible.
