LeetCode 881 - Boats to Save People
This problem asks us to determine the minimum number of boats required to rescue all people, given two constraints. Each boat can carry at most two people, and the combined weight of the people in a boat cannot exceed the given weight limit.
Difficulty: 🟡 Medium
Topics: Array, Two Pointers, Greedy, Sorting
Solution
LeetCode 881, Boats to Save People
Problem Understanding
This problem asks us to determine the minimum number of boats required to rescue all people, given two constraints. Each boat can carry at most two people, and the combined weight of the people in a boat cannot exceed the given weight limit.
The input consists of:
people, an array where each value represents the weight of one personlimit, the maximum total weight a single boat can carry
The goal is to place every person into some boat while minimizing the total number of boats used.
For example, if people = [1,2] and limit = 3, both people can share a boat because 1 + 2 = 3, which does not exceed the limit. Therefore, only one boat is needed.
The problem guarantees that every individual person's weight is less than or equal to the boat limit, so every person can always fit into a boat alone if necessary. This is important because it means there is always at least one valid solution.
The constraints are large enough that efficiency matters:
- Up to
5 * 10^4people - Weights and limits up to
3 * 10^4
A quadratic solution that repeatedly searches for partners for each person would likely be too slow. We need something closer to O(n log n) or better.
Several edge cases are important to think about upfront:
- Everyone may need their own boat because no pair fits within the limit.
- Every pair may fit perfectly, allowing maximal sharing.
- The array may contain many duplicate weights.
- There may be only one person.
- The heaviest person may only fit alone, which affects how we pair the remaining people.
The central challenge is finding an efficient strategy for pairing people together whenever possible without wasting boat capacity.
Approaches
Brute Force Approach
A straightforward brute force solution would repeatedly try to pair each person with another remaining person whose combined weight stays within the limit.
One possible implementation is:
- Pick an unassigned person.
- Search all remaining unassigned people for the heaviest valid partner.
- Assign them to a boat.
- Repeat until everyone is assigned.
This approach is correct because it eventually places everyone into boats while respecting the constraints. However, the repeated searching makes it inefficient.
If we search through the remaining people for every person, the time complexity becomes O(n^2). With up to 50,000 people, this can become far too slow.
Optimal Greedy Two Pointer Approach
The key insight is that sorting allows us to make optimal pairing decisions efficiently.
After sorting the weights:
- The lightest person is at the beginning.
- The heaviest person is at the end.
The heaviest person is the hardest to place because they have the fewest possible partners. Therefore, we should always process the heaviest remaining person first.
For each heaviest person:
- If they can share a boat with the lightest remaining person, we should pair them together.
- If they cannot share with the lightest person, then they cannot share with anyone else either, because everyone else is heavier.
This observation makes a greedy strategy optimal.
We use two pointers:
leftfor the lightest remaining personrightfor the heaviest remaining person
At every step:
- Try pairing
people[left] + people[right] - If the pair fits, move both pointers
- Otherwise, the heaviest person goes alone, so move only
right - In both cases, one boat is used
This gives an efficient O(n log n) solution because sorting dominates the runtime.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(n) | Repeatedly searches for valid partners |
| Optimal | O(n log n) | O(1) or O(log n) depending on sort | Sorts once, then uses greedy two pointers |
Algorithm Walkthrough
- Sort the
peoplearray in nondecreasing order.
Sorting allows us to efficiently compare the lightest and heaviest remaining people. This structure is what enables the greedy strategy. 2. Initialize two pointers.
Set:
left = 0for the lightest personright = len(people) - 1for the heaviest person
Also initialize boats = 0.
3. Process people while left <= right.
As long as there are unassigned people, we continue assigning boats. 4. Check whether the lightest and heaviest people can share a boat.
If:
$people[left] + people[right] \le limit$
then they can ride together.
In this case:
- Increment
left - Decrement
right
We successfully placed two people into one boat. 5. Otherwise, the heaviest person must go alone.
If the lightest remaining person cannot pair with the heaviest person, then nobody can pair with the heaviest person because everyone else is heavier.
So:
- Decrement
right
Only the heaviest person is assigned to this boat. 6. Increment the boat count.
Every iteration uses exactly one boat. 7. Continue until all people are assigned.
When left > right, everyone has been processed.
8. Return the total number of boats.
Why it works
The correctness comes from always making the best possible placement for the heaviest remaining person.
If the heaviest person can pair with the lightest person, pairing them is optimal because it uses one boat efficiently while preserving heavier people for future pairings.
If the heaviest person cannot pair with the lightest person, they cannot pair with anyone else. Therefore, sending them alone is unavoidable and cannot be improved.
This greedy choice is always safe, which guarantees an optimal solution.
Python Solution
from typing import List
class Solution:
def numRescueBoats(self, people: List[int], limit: int) -> int:
people.sort()
left = 0
right = len(people) - 1
boats = 0
while left <= right:
if people[left] + people[right] <= limit:
left += 1
right -= 1
boats += 1
return boats
The implementation begins by sorting the array so that the lightest and heaviest people can be accessed efficiently using pointers.
The left pointer starts at the beginning of the array, while the right pointer starts at the end. The variable boats tracks how many boats have been used.
Inside the loop, the algorithm checks whether the lightest and heaviest remaining people can share a boat. If they can, the lightest person is paired with the heaviest person, so left moves forward.
Regardless of whether pairing succeeds, the heaviest person is always assigned to a boat during the current iteration, so right always moves backward and boats always increases by one.
The loop stops once all people have been assigned to boats.
Go Solution
package main
import "sort"
func numRescueBoats(people []int, limit int) int {
sort.Ints(people)
left := 0
right := len(people) - 1
boats := 0
for left <= right {
if people[left]+people[right] <= limit {
left++
}
right--
boats++
}
return boats
}
The Go implementation follows the same logic as the Python version. The primary difference is the use of sort.Ints from Go's standard library to sort the slice in place.
Go slices are reference-like structures, so sorting modifies the original slice directly. Integer overflow is not a concern here because the maximum possible sum is 60000, which easily fits within Go's int type.
The implementation naturally handles empty and single-element slices correctly, although the constraints guarantee at least one person.
Worked Examples
Example 1
Input:
people = [1,2]
limit = 3
After sorting:
[1,2]
| Step | left | right | Pair Attempt | Result | Boats |
|---|---|---|---|---|---|
| 1 | 0 | 1 | 1 + 2 = 3 | Pair together | 1 |
Final answer:
1
Example 2
Input:
people = [3,2,2,1]
limit = 3
After sorting:
[1,2,2,3]
| Step | left | right | Pair Attempt | Result | Boats |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 1 + 3 = 4 | 3 goes alone | 1 |
| 2 | 0 | 2 | 1 + 2 = 3 | Pair together | 2 |
| 3 | 1 | 1 | 2 alone | Single person | 3 |
Final answer:
3
Boat assignments:
(3), (1,2), (2)
Example 3
Input:
people = [3,5,3,4]
limit = 5
After sorting:
[3,3,4,5]
| Step | left | right | Pair Attempt | Result | Boats |
|---|---|---|---|---|---|
| 1 | 0 | 3 | 3 + 5 = 8 | 5 goes alone | 1 |
| 2 | 0 | 2 | 3 + 4 = 7 | 4 goes alone | 2 |
| 3 | 0 | 1 | 3 + 3 = 6 | One 3 goes alone | 3 |
| 4 | 0 | 0 | 3 alone | Single person | 4 |
Final answer:
4
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting dominates the runtime |
| Space | O(1) or O(log n) | Depends on sorting implementation |
The two pointer traversal itself is linear because each pointer moves only in one direction and every person is processed once.
The sorting step requires O(n log n) time, which dominates the overall complexity.
The algorithm uses only a few extra variables beyond the input array. However, the sorting implementation may use additional stack space internally, which is why some analyses report O(log n) auxiliary space.
Test Cases
from typing import List
class Solution:
def numRescueBoats(self, people: List[int], limit: int) -> int:
people.sort()
left = 0
right = len(people) - 1
boats = 0
while left <= right:
if people[left] + people[right] <= limit:
left += 1
right -= 1
boats += 1
return boats
sol = Solution()
assert sol.numRescueBoats([1, 2], 3) == 1 # simple pair fits exactly
assert sol.numRescueBoats([3, 2, 2, 1], 3) == 3 # mix of paired and solo boats
assert sol.numRescueBoats([3, 5, 3, 4], 5) == 4 # nobody can pair
assert sol.numRescueBoats([1], 3) == 1 # single person
assert sol.numRescueBoats([2, 2], 4) == 1 # exact limit pairing
assert sol.numRescueBoats([2, 2], 3) == 2 # pair exceeds limit
assert sol.numRescueBoats([1, 1, 1, 1], 2) == 2 # all pair perfectly
assert sol.numRescueBoats([5, 5, 5, 5], 5) == 4 # everyone must go alone
assert sol.numRescueBoats([1, 2, 3, 4, 5], 5) == 3 # mixed pairings
assert sol.numRescueBoats([1, 1, 2, 2], 3) == 2 # optimal greedy matching
assert sol.numRescueBoats([3, 2, 3, 2, 2], 6) == 3 # larger pairing combinations
assert sol.numRescueBoats([1, 2, 2, 3], 4) == 2 # multiple valid pairings
print("All test cases passed!")
| Test | Why |
|---|---|
[1,2], limit=3 |
Simplest successful pairing |
[3,2,2,1], limit=3 |
Mix of paired and solo boats |
[3,5,3,4], limit=5 |
No valid pairings |
[1], limit=3 |
Single-element edge case |
[2,2], limit=4 |
Exact limit pairing |
[2,2], limit=3 |
Pair just exceeds limit |
[1,1,1,1], limit=2 |
Every boat fully utilized |
[5,5,5,5], limit=5 |
Everyone forced alone |
[1,2,3,4,5], limit=5 |
General mixed scenario |
[1,1,2,2], limit=3 |
Greedy matching validation |
[3,2,3,2,2], limit=6 |
Multiple optimal pairings |
[1,2,2,3], limit=4 |
Confirms pointer movement correctness |
Edge Cases
Everyone Must Ride Alone
A common edge case occurs when no two people can share a boat. For example:
people = [4,4,4]
limit = 4
Each person already reaches the weight limit individually, so pairing is impossible. A buggy implementation might still attempt invalid pairings or mishandle pointer movement.
The two pointer solution handles this naturally because every pairing check fails, causing the heaviest person to be assigned alone each iteration.
Perfect Pairing Scenario
Another important case is when every boat can hold exactly two people efficiently, such as:
people = [1,1,2,2]
limit = 3
A poor greedy strategy could waste space by pairing smaller people together too early.
The sorted two pointer approach avoids this issue because it always tries to pair the heaviest remaining person first, ensuring optimal boat usage.
Single Remaining Person
Eventually, the algorithm may reach a state where only one person remains:
left == right
This can be a source of off-by-one bugs if the loop condition is incorrect.
The implementation uses:
while left <= right:
This guarantees the final unpaired person still receives a boat before the loop exits.