Problem overview
The method minBoats receives the students' weights, a boat capacity maxWeight and a limit maxDiff. Boats are unlimited, and each carries one or two students. Two students may share a boat only if their weights add up to at most maxWeight and differ by at most maxDiff. The method returns the smallest number of boats that carries everyone.
For weights = [2, 4, 2, 1], maxWeight = 6 and maxDiff = 1, the answer is 3. The student of weight 4 can't pair with anyone, because even the closest weight, 2, differs by 2. The 1 and a 2 fit together, and the remaining 2 rides alone, or the two 2s share a boat and the 1 rides alone. Either way, three boats. For the second example, with eight students, maxWeight = 100 and maxDiff = 10, the answer is 6.
Approach: heaviest first, closest lighter partner
Every pair that shares a boat saves one boat, so the goal is to form as many valid pairs as possible. The answer is the number of students minus the number of pairs.
The solution sorts the weights and processes students from the heaviest to the lightest. For the current student i, the best partner is a lighter student who fits by total weight and whose weight is as close to weights(i) as possible, since closeness is what the maxDiff rule measures. Candidates are collected on a stack: a second pointer p starts at the lightest student and moves right while weights(p) + weights(i) <= maxWeight, pushing each student it passes. Because p moves over the sorted array in ascending order, the last student pushed is the heaviest candidate, so the top of the stack is the closest in weight to weights(i). If the top is too far away (a difference above maxDiff), every other candidate is too.
A candidate pushed for one student still fits the next one, because the next student is lighter or equal, so the sum can only shrink. That is why p never has to move back, and each student is pushed at most once.
Traced on the sorted weights [1, 2, 2, 4] with maxWeight = 6 and maxDiff = 1:
i = 3(weight 4):ppushes the weights 1, 2 and 2, since each sums to at most 6 with the 4. The top is a 2, which is 2 below the 4, more thanmaxDiff. The 4 gets its own boat.i = 2(weight 2): the top of the stack is this student itself, which is thrown away. The next top, the 2 at index 1, is a valid partner. Both are marked as taken, and one boat is counted.i = 1: already taken, so it is skipped.i = 0(weight 1): the only candidate left on the stack is itself, so it is discarded and it gets its own boat.
That is three boats.
Step by step
- Sort
weightsin ascending order. - Declare
answerandpasvars set to 0.pis the pointer that feeds the stack from the light end. - Create
isTaken, an empty mutableSet[Int]of indices of students who already have a partner. - Create
stack, anArrayBuffer[(Int, Int)]of (weight, index) pairs. Pushes come in ascending weight, so a plain stack does the work of a max heap here. - Walk from the heaviest student:
var i = weights.length - 1andwhile (i >= 0). The loop decrementsiitself. - Feed the stack. An inner
whileruns as long asp < i && weights(p) + weights(i) <= maxWeight. - Push
(weights(p), p)ontostackand incrementp. - Skip students already paired. If
isTaken.contains(i), just decrementi. Scala has nocontinue, so everything else goes in theelsebranch. - Look for a partner. In the
elsebranch declarevar done = false, a flag that replaces the missingbreak, and loop while!done, the stack is non-empty andweights(i) - stack(stack.size - 1)._1 <= maxDiff. - Discard useless tops. Read the top into
val top. Iftop._2 == i || isTaken.contains(top._2), remove the last element of the stack. - Pair up otherwise. Add both
iandtop._2toisTaken. - Finish the search. Remove the last element of the stack and set
done = true. - Count the boat. Below the inner loop, increment
answerand decrementi. A pair counts once, at its heavier student; the lighter one is skipped later. - Return
answeras the last expression.
The solution in Scala
class Solution {
def minBoats(weights: Array[Int], maxWeight: Int, maxDiff: Int): Int = {
scala.util.Sorting.quickSort(weights)
var answer = 0
var p = 0
val isTaken = scala.collection.mutable.Set[Int]()
val stack = scala.collection.mutable.ArrayBuffer[(Int, Int)]()
var i = weights.length - 1
while (i >= 0) {
while (p < i && weights(p) + weights(i) <= maxWeight) {
stack += ((weights(p), p))
p += 1
}
if (isTaken.contains(i)) {
i -= 1
} else {
var done = false
while (!done && stack.nonEmpty && weights(i) - stack(stack.size - 1)._1 <= maxDiff) {
val top = stack(stack.size - 1)
if (top._2 == i || isTaken.contains(top._2)) {
stack.remove(stack.size - 1)
} else {
isTaken.add(i)
isTaken.add(top._2)
stack.remove(stack.size - 1)
done = true
}
}
answer += 1
i -= 1
}
}
answer
}
}
The Scala details worth a closer look:
- Sorting changes the caller's array.
scala.util.Sorting.quickSort(weights)sorts anArray[Int]in place and returnsUnit. AfterminBoatsruns on[2, 4, 2, 1], the caller's array reads[1, 2, 2, 4]. Copy the array first, withweights.clone(), if the original order matters. - Tuples.
(weights(p), p)is a pair of type(Int, Int), read back with._1and._2, which count from 1. Instack += ((weights(p), p)), the inner parentheses build the tuple and the outer ones are the call to+=. - An
ArrayBufferas a stack. The last element is the top:stack(stack.size - 1)reads it andstack.remove(stack.size - 1)pops it, both in constant time.nonEmptyneeds no parentheses. - The mutable set.
scala.collection.mutable.Set[Int]()is named in full because the defaultSetis immutable.addreturns aBooleanthat the code ignores. - No
breakorcontinue. Thedoneflag and theelsebranch do their jobs.
Complexity
Time: O(n log n). Sorting costs O(n log n). Afterwards each student is pushed at most once, because p only moves forward, and removed from the stack at most once. The index i moves down by one per iteration. All set operations take constant time on average. The sweep is therefore O(n).
Space: O(n). The stack and the set each hold at most n entries.
Edge cases and common mistakes
- No students. The main loop doesn't run, so the answer is 0.
- One student.
[5]returns 1: nobody else is available. - Weight fits but difference doesn't.
[1, 5]withmaxWeight = 6andmaxDiff = 1returns 2, but withmaxDiff = 4it returns 1. - Difference fits but weight doesn't.
[3, 4]withmaxWeight = 6andmaxDiff = 5returns 2, since 3 + 4 is 7. - Equal weights.
[3, 3]withmaxWeight = 6andmaxDiff = 0returns 1. A student must never pair with themselves, which is why the code comparestop._2withi. - Counting a boat for the lighter partner. The lighter student is skipped through
isTaken. Counting it again would give one boat too many for every pair. - Using the lightest student as everyone's partner. It satisfies the weight limit but may break
maxDiff. The stack keeps the closest valid weight instead. - Relying on the original order. The method reorders its argument, as described above.
Testing
weights = [2, 4, 2, 1],maxWeight = 6,maxDiff = 1gives3.weights = [81, 37, 32, 88, 55, 93, 45, 72],maxWeight = 100,maxDiff = 10gives6.weights = [1, 5],maxWeight = 6,maxDiff = 4gives1.weights = [3, 4],maxWeight = 6,maxDiff = 5gives2.weights = [],maxWeight = 6,maxDiff = 1gives0.
