1687. Delivering Boxes from Storage to Ports
Problem Description
You need to deliver boxes from a storage warehouse to various ports using a single ship. The ship has constraints on both the number of boxes it can carry and the total weight capacity.
You're given an array boxes
where each element boxes[i] = [ports_i, weight_i]
contains:
ports_i
: the destination port for the i-th boxweight_i
: the weight of the i-th box
You're also given:
portsCount
: the total number of portsmaxBoxes
: maximum number of boxes the ship can carry at oncemaxWeight
: maximum total weight the ship can carry at once
Important constraints:
- Boxes must be delivered in the exact order they appear in the array
- The ship starts at the storage warehouse
How the delivery process works:
- The ship loads some consecutive boxes from the front of the queue (respecting both
maxBoxes
andmaxWeight
limits) - The ship delivers each loaded box in order to its destination port:
- If the ship is already at the correct port, no trip is needed
- If the ship needs to go to a different port, it makes one trip
- After delivering all loaded boxes, the ship makes a return trip to the storage warehouse
- The process repeats until all boxes are delivered
Counting trips:
- Going from storage to the first port counts as 1 trip
- Moving between different ports counts as 1 trip per move
- Returning from the last port to storage counts as 1 trip
- Staying at the same port for consecutive deliveries counts as 0 trips
Your task is to find the minimum number of trips needed to deliver all boxes to their respective ports.
Example scenario: If the ship takes boxes going to ports [4, 4, 5]:
- Trip 1: Storage → Port 4
- Trip 0: Stay at Port 4 (second box also goes here)
- Trip 1: Port 4 → Port 5
- Trip 1: Port 5 → Storage
- Total: 3 trips for this batch
The challenge is to optimally group consecutive boxes for each loading while respecting the ship's capacity constraints to minimize the total number of trips.
Intuition
Let's think about this problem step by step. Since boxes must be delivered in order, we're essentially deciding where to "cut" the sequence - which groups of consecutive boxes should be loaded together in each batch.
For any batch of consecutive boxes from index j+1
to i
, we can calculate the number of trips needed:
- 1 trip from storage to the first port
- Additional trips between different consecutive ports (if port changes)
- 1 trip back to storage
- Total = 2 + (number of port changes within the batch)
This naturally leads to a dynamic programming approach. Let f[i]
represent the minimum trips needed to deliver the first i
boxes. To compute f[i]
, we need to consider all valid previous positions j
where we could have ended the previous batch, then taken boxes j+1
through i
as the current batch.
The key insight is that for any batch [j+1, ..., i]
:
- It must satisfy weight constraint:
sum(weights[j+1]...weights[i]) ≤ maxWeight
- It must satisfy box count constraint:
i - j ≤ maxBoxes
- The trip cost is:
2 + (number of port changes in range)
To efficiently calculate port changes, we can use prefix sums. If we create an array c
where c[k] = 1
if port[k] != port[k+1]
, then the number of port changes from position j+1
to i
is simply cs[i-1] - cs[j]
where cs
is the prefix sum of c
.
The state transition becomes:
f[i] = min(f[j] + cs[i-1] - cs[j] + 2)
for all valid j
However, with data scale reaching 10^5
, the O(n²) approach times out. We need optimization.
Looking at the transition equation more carefully:
f[i] = min(f[j] - cs[j]) + cs[i-1] + 2
The term cs[i-1] + 2
is constant for a fixed i
. We're essentially finding the minimum value of f[j] - cs[j]
among all valid j
values in the sliding window [i-maxBoxes, i-1]
that also satisfy the weight constraint.
This is a classic sliding window minimum problem! We can use a monotonic deque to maintain potential candidates:
- Keep indices in increasing order of
f[j] - cs[j]
values - Remove indices that fall outside the box count window
- Remove indices that violate the weight constraint
- The front of the deque gives us the optimal
j
for currenti
This optimization reduces the complexity from O(n²) to O(n), making the solution efficient enough for the given constraints.
Learn more about Segment Tree, Queue, Dynamic Programming, Prefix Sum, Monotonic Queue and Heap (Priority Queue) patterns.
Solution Approach
Let's implement the optimized dynamic programming solution with monotonic queue step by step.
Step 1: Preprocessing
First, we need to prepare auxiliary arrays for efficient calculation:
n = len(boxes)
ws = list(accumulate((box[1] for box in boxes), initial=0))
c = [int(a != b) for a, b in pairwise(box[0] for box in boxes)]
cs = list(accumulate(c, initial=0))
ws[i]
: prefix sum of weights, wherews[i]
= sum of weights from box 0 to box i-1c[i]
: 1 if port changes between box i and i+1, 0 otherwisecs[i]
: prefix sum of port changes
Step 2: Dynamic Programming with Monotonic Queue
Initialize the DP array and deque:
f = [0] * (n + 1) # f[i] = minimum trips to deliver first i boxes q = deque([0]) # monotonic [queue](/problems/queue_intro) storing indices
Step 3: Main DP Loop
For each position i
from 1 to n:
for i in range(1, n + 1):
# Remove invalid indices from front of deque
while q and (i - q[0] > maxBoxes or ws[i] - ws[q[0]] > maxWeight):
q.popleft()
This step removes indices that:
- Violate box count constraint:
i - j > maxBoxes
- Violate weight constraint:
ws[i] - ws[j] > maxWeight
Step 4: Calculate Minimum Trips
if q: f[i] = cs[i - 1] + f[q[0]] - cs[q[0]] + 2
The formula breaks down as:
f[q[0]]
: minimum trips to deliver firstq[0]
boxescs[i-1] - cs[q[0]]
: port changes between boxesq[0]+1
toi
+2
: one trip from storage to first port, one trip back to storage
Step 5: Maintain Monotonic Property
if i < n: while q and f[q[-1]] - cs[q[-1]] >= f[i] - cs[i]: q.pop() q.append(i)
We maintain the deque in increasing order of f[j] - cs[j]
values:
- Remove indices from the back if their value is not better than current
i
- Add current index
i
to the deque (only if not the last position)
Why This Works:
The monotonic queue ensures that:
- The front element always gives the minimum value of
f[j] - cs[j]
in the valid window - Outdated or suboptimal indices are efficiently removed
- Each index is added and removed at most once, giving O(n) complexity
Time Complexity: O(n) - each element is processed once through the deque Space Complexity: O(n) - for the auxiliary arrays and DP array
The final answer is f[n]
, representing the minimum trips needed to deliver all n boxes.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand the solution approach.
Input:
boxes = [[1,2], [3,3], [3,1], [3,1], [4,4]]
portsCount = 4
,maxBoxes = 3
,maxWeight = 6
Step 1: Preprocessing
First, we build our auxiliary arrays:
- Weights:
[2, 3, 1, 1, 4]
- Weight prefix sums
ws
:[0, 2, 5, 6, 7, 11]
- Port changes
c
:[1, 0, 0, 1]
(comparing ports: 1→3, 3→3, 3→3, 3→4) - Port change prefix sums
cs
:[0, 1, 1, 1, 2]
Step 2: Initialize DP
f = [0, 0, 0, 0, 0, 0]
(will fill positions 1-5)q = deque([0])
(monotonic queue starts with index 0)
Step 3: Process Each Position
i = 1 (Delivering first box [1,2]):
- Check deque front: Can we take boxes from position 0 to 1?
- Box count: 1 - 0 = 1 ≤ 3 ✓
- Weight: ws[1] - ws[0] = 2 - 0 = 2 ≤ 6 ✓
- Calculate:
f[1] = cs[0] + f[0] - cs[0] + 2 = 0 + 0 - 0 + 2 = 2
- This means: storage→port1→storage (2 trips)
- Update deque: Add index 1
- State:
f = [0, 2, 0, 0, 0, 0]
,q = [0, 1]
i = 2 (Delivering first two boxes):
- Check deque front (index 0):
- Box count: 2 - 0 = 2 ≤ 3 ✓
- Weight: ws[2] - ws[0] = 5 - 0 = 5 ≤ 6 ✓
- Calculate:
f[2] = cs[1] + f[0] - cs[0] + 2 = 1 + 0 - 0 + 2 = 3
- Taking both boxes: storage→port1→port3→storage (3 trips)
- Alternative from index 1:
f[2] = cs[1] + f[1] - cs[1] + 2 = 1 + 2 - 1 + 2 = 4
- Taking second box alone after first: more trips
- Deque maintenance: index 0 gives better value (0 - 0 = 0) than index 1 (2 - 1 = 1)
- State:
f = [0, 2, 3, 0, 0, 0]
,q = [0, 2]
i = 3 (Delivering first three boxes):
- Check deque front (index 0):
- Box count: 3 - 0 = 3 ≤ 3 ✓
- Weight: ws[3] - ws[0] = 6 - 0 = 6 ≤ 6 ✓
- Calculate:
f[3] = cs[2] + f[0] - cs[0] + 2 = 1 + 0 - 0 + 2 = 3
- All three boxes: storage→port1→port3(stay)→port3(stay)→storage (3 trips)
- State:
f = [0, 2, 3, 3, 0, 0]
,q = [0, 3]
i = 4 (Delivering first four boxes):
- Check deque front (index 0):
- Box count: 4 - 0 = 4 > 3 ✗ (exceeds maxBoxes)
- Remove index 0 from deque
- Next in deque (index 3):
- Box count: 4 - 3 = 1 ≤ 3 ✓
- Weight: ws[4] - ws[3] = 7 - 6 = 1 ≤ 6 ✓
- Calculate:
f[4] = cs[3] + f[3] - cs[3] + 2 = 1 + 3 - 1 + 2 = 5
- Take 4th box alone after first three
- State:
f = [0, 2, 3, 3, 5, 0]
,q = [3, 4]
i = 5 (Delivering all five boxes):
- Check deque front (index 3):
- Box count: 5 - 3 = 2 ≤ 3 ✓
- Weight: ws[5] - ws[3] = 11 - 6 = 5 ≤ 6 ✓
- Calculate:
f[5] = cs[4] + f[3] - cs[3] + 2 = 2 + 3 - 1 + 2 = 6
- Take last two boxes together after first three
- Final answer:
f[5] = 6
Verification:
- Batch 1: Boxes [1,2], [3,3], [3,1] → storage→port1→port3→storage (3 trips)
- Batch 2: Boxes [3,1], [4,4] → storage→port3→port4→storage (3 trips)
- Total: 6 trips ✓
Solution Implementation
1class Solution:
2 def boxDelivering(
3 self, boxes: List[List[int]], portsCount: int, maxBoxes: int, maxWeight: int
4 ) -> int:
5 n = len(boxes)
6
7 # Prefix sum of weights for quick range sum calculation
8 weight_prefix_sum = list(accumulate((box[1] for box in boxes), initial=0))
9
10 # Count transitions between different ports (1 if port changes, 0 otherwise)
11 port_changes = [int(a != b) for a, b in pairwise(box[0] for box in boxes)]
12
13 # Prefix sum of port changes for quick range calculation
14 changes_prefix_sum = list(accumulate(port_changes, initial=0))
15
16 # dp[i] represents minimum trips needed to deliver first i boxes
17 dp = [0] * (n + 1)
18
19 # Monotonic deque to maintain optimal starting points for current batch
20 deque_indices = deque([0])
21
22 for i in range(1, n + 1):
23 # Remove indices that violate constraints (too many boxes or too heavy)
24 while deque_indices and (
25 i - deque_indices[0] > maxBoxes or
26 weight_prefix_sum[i] - weight_prefix_sum[deque_indices[0]] > maxWeight
27 ):
28 deque_indices.popleft()
29
30 # Calculate minimum trips if we have valid starting points
31 if deque_indices:
32 # Trips = previous trips + port changes in current batch + 2 (pickup and delivery)
33 start_idx = deque_indices[0]
34 dp[i] = (changes_prefix_sum[i - 1] + dp[start_idx] -
35 changes_prefix_sum[start_idx] + 2)
36
37 # Prepare for next iteration (only if not at last box)
38 if i < n:
39 # Maintain monotonic property: remove suboptimal starting points
40 while deque_indices and (
41 dp[deque_indices[-1]] - changes_prefix_sum[deque_indices[-1]] >=
42 dp[i] - changes_prefix_sum[i]
43 ):
44 deque_indices.pop()
45 deque_indices.append(i)
46
47 return dp[n]
48
1class Solution {
2 public int boxDelivering(int[][] boxes, int portsCount, int maxBoxes, int maxWeight) {
3 int n = boxes.length;
4
5 // Prefix sum array for weights: prefixWeight[i] = sum of weights from box 0 to box i-1
6 long[] prefixWeight = new long[n + 1];
7
8 // Cost array: portChanges[i] = number of port changes from box 0 to box i
9 int[] portChanges = new int[n];
10
11 // Build prefix weight array and count port changes
12 for (int i = 0; i < n; i++) {
13 int port = boxes[i][0];
14 int weight = boxes[i][1];
15
16 // Calculate prefix sum of weights
17 prefixWeight[i + 1] = prefixWeight[i] + weight;
18
19 // Count port changes between consecutive boxes
20 if (i < n - 1) {
21 portChanges[i + 1] = portChanges[i] + (port != boxes[i + 1][0] ? 1 : 0);
22 }
23 }
24
25 // DP array: minTrips[i] = minimum trips to deliver first i boxes
26 int[] minTrips = new int[n + 1];
27
28 // Deque for monotonic queue optimization
29 Deque<Integer> monotonicQueue = new ArrayDeque<>();
30 monotonicQueue.offer(0);
31
32 // Dynamic programming with sliding window optimization
33 for (int i = 1; i <= n; i++) {
34 // Remove indices from front that violate constraints (too many boxes or too much weight)
35 while (!monotonicQueue.isEmpty() &&
36 (i - monotonicQueue.peekFirst() > maxBoxes ||
37 prefixWeight[i] - prefixWeight[monotonicQueue.peekFirst()] > maxWeight)) {
38 monotonicQueue.pollFirst();
39 }
40
41 // Calculate minimum trips for delivering first i boxes
42 if (!monotonicQueue.isEmpty()) {
43 int j = monotonicQueue.peekFirst();
44 // minTrips[i] = minTrips[j] + (trips from j to i)
45 // trips from j to i = 2 (go and return) + port changes in range [j, i-1]
46 minTrips[i] = portChanges[i - 1] + minTrips[j] - portChanges[j] + 2;
47 }
48
49 // Maintain monotonic property of the queue for next iterations
50 if (i < n) {
51 // Remove elements from back that are not optimal
52 // Keep only indices j where minTrips[j] - portChanges[j] is minimal
53 while (!monotonicQueue.isEmpty() &&
54 minTrips[monotonicQueue.peekLast()] - portChanges[monotonicQueue.peekLast()] >=
55 minTrips[i] - portChanges[i]) {
56 monotonicQueue.pollLast();
57 }
58 monotonicQueue.offer(i);
59 }
60 }
61
62 return minTrips[n];
63 }
64}
65
1class Solution {
2public:
3 int boxDelivering(vector<vector<int>>& boxes, int portsCount, int maxBoxes, int maxWeight) {
4 int n = boxes.size();
5
6 // Prefix sum array for weights: weightSum[i] = sum of weights from box 0 to box i-1
7 vector<long> weightSum(n + 1, 0);
8
9 // DP array: minTrips[i] = minimum trips to deliver first i boxes
10 vector<int> minTrips(n + 1, 0);
11
12 // Cost array: portChanges[i] = number of port changes from box 0 to box i
13 vector<int> portChanges(n, 0);
14
15 // Build prefix sum of weights and count port changes
16 for (int i = 0; i < n; ++i) {
17 int port = boxes[i][0];
18 int weight = boxes[i][1];
19 weightSum[i + 1] = weightSum[i] + weight;
20
21 // Count port changes: increment if current port differs from next port
22 if (i < n - 1) {
23 portChanges[i + 1] = portChanges[i] + (port != boxes[i + 1][0] ? 1 : 0);
24 }
25 }
26
27 // Deque for sliding window optimization (stores indices)
28 deque<int> dq;
29 dq.push_back(0);
30
31 // Dynamic programming to find minimum trips
32 for (int i = 1; i <= n; ++i) {
33 // Remove indices that violate box count or weight constraints
34 while (!dq.empty() &&
35 (i - dq.front() > maxBoxes ||
36 weightSum[i] - weightSum[dq.front()] > maxWeight)) {
37 dq.pop_front();
38 }
39
40 // Calculate minimum trips for delivering first i boxes
41 if (!dq.empty()) {
42 int j = dq.front();
43 // minTrips[i] = minTrips[j] + (port changes from j to i-1) + 2
44 // The +2 accounts for: 1 trip from warehouse to first port, 1 trip back to warehouse
45 minTrips[i] = portChanges[i - 1] + minTrips[j] - portChanges[j] + 2;
46 }
47
48 // Maintain monotonic deque for next iterations
49 if (i < n) {
50 // Remove indices with worse or equal value (minTrips[k] - portChanges[k])
51 while (!dq.empty() &&
52 minTrips[dq.back()] - portChanges[dq.back()] >= minTrips[i] - portChanges[i]) {
53 dq.pop_back();
54 }
55 dq.push_back(i);
56 }
57 }
58
59 return minTrips[n];
60 }
61};
62
1function boxDelivering(boxes: number[][], portsCount: number, maxBoxes: number, maxWeight: number): number {
2 const n = boxes.length;
3
4 // Prefix sum array for weights: weightSum[i] = sum of weights from box 0 to box i-1
5 const weightSum: number[] = new Array(n + 1).fill(0);
6
7 // DP array: minTrips[i] = minimum trips to deliver first i boxes
8 const minTrips: number[] = new Array(n + 1).fill(0);
9
10 // Cost array: portChanges[i] = number of port changes from box 0 to box i
11 const portChanges: number[] = new Array(n).fill(0);
12
13 // Build prefix sum of weights and count port changes
14 for (let i = 0; i < n; i++) {
15 const port = boxes[i][0];
16 const weight = boxes[i][1];
17 weightSum[i + 1] = weightSum[i] + weight;
18
19 // Count port changes: increment if current port differs from next port
20 if (i < n - 1) {
21 portChanges[i + 1] = portChanges[i] + (port !== boxes[i + 1][0] ? 1 : 0);
22 }
23 }
24
25 // Deque for sliding window optimization (stores indices)
26 const deque: number[] = [];
27 deque.push(0);
28
29 // Dynamic programming to find minimum trips
30 for (let i = 1; i <= n; i++) {
31 // Remove indices that violate box count or weight constraints
32 while (deque.length > 0 &&
33 (i - deque[0] > maxBoxes ||
34 weightSum[i] - weightSum[deque[0]] > maxWeight)) {
35 deque.shift();
36 }
37
38 // Calculate minimum trips for delivering first i boxes
39 if (deque.length > 0) {
40 const j = deque[0];
41 // minTrips[i] = minTrips[j] + (port changes from j to i-1) + 2
42 // The +2 accounts for: 1 trip from warehouse to first port, 1 trip back to warehouse
43 minTrips[i] = portChanges[i - 1] + minTrips[j] - portChanges[j] + 2;
44 }
45
46 // Maintain monotonic deque for next iterations
47 if (i < n) {
48 // Remove indices with worse or equal value (minTrips[k] - portChanges[k])
49 while (deque.length > 0 &&
50 minTrips[deque[deque.length - 1]] - portChanges[deque[deque.length - 1]] >= minTrips[i] - portChanges[i]) {
51 deque.pop();
52 }
53 deque.push(i);
54 }
55 }
56
57 return minTrips[n];
58}
59
Time and Space Complexity
Time Complexity: O(n)
, where n
is the number of boxes.
The algorithm processes each box exactly once through the following operations:
- Building the weight prefix sum array
ws
takesO(n)
time - Creating the consecutive port difference array
c
takesO(n)
time - Building the prefix sum array
cs
takesO(n)
time - The main dynamic programming loop runs
n
iterations- Each iteration performs deque operations (append, pop, popleft)
- While there are nested while loops, each element is added to and removed from the deque at most once throughout the entire algorithm
- This amortizes to
O(1)
per iteration
- Total:
O(n) + O(n) + O(n) + O(n) = O(n)
Space Complexity: O(n)
, where n
is the number of boxes.
The algorithm uses the following data structures:
ws
: weight prefix sum array of sizen + 1
c
: consecutive port difference array of sizen - 1
cs
: prefix sum of differences array of sizen
f
: dynamic programming array of sizen + 1
q
: deque that can contain at mostmin(n, maxBoxes)
elements, which isO(n)
in the worst case- Total:
O(n) + O(n) + O(n) + O(n) + O(n) = O(n)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Errors in Index Management
Pitfall: The most common mistake is confusion between box indices and DP array indices. The DP array dp[i]
represents the state after delivering the first i
boxes (0-indexed), while boxes[i-1]
is the i-th box. This leads to errors when:
- Calculating weight sums: Using
weight_prefix_sum[i] - weight_prefix_sum[j]
for boxes j to i-1 - Accessing port changes: Using
changes_prefix_sum[i-1]
for changes up to box i-1
Solution: Always be explicit about what each index represents:
# When dp[i] represents first i boxes delivered: # - The last box included is boxes[i-1] # - Weight from box j to i-1: weight_prefix_sum[i] - weight_prefix_sum[j] # - Port changes up to box i-1: changes_prefix_sum[i-1]
2. Incorrect Monotonic Queue Maintenance
Pitfall: Forgetting to skip adding the last index n
to the deque. Since dp[n]
is the final answer and won't be used as a starting point for future batches, adding it causes unnecessary comparisons and potential errors.
Solution: Add the guard condition before deque maintenance:
if i < n: # Critical: Don't add the last position to deque while deque_indices and (...): deque_indices.pop() deque_indices.append(i)
3. Misunderstanding Port Change Calculation
Pitfall: Incorrectly calculating port changes when the ship delivers multiple boxes. Remember that consecutive boxes to the same port require 0 additional trips, not 1 trip each.
Example Error:
# WRONG: Counting each box delivery as a separate trip trips = number_of_boxes + 1 # Incorrect! # CORRECT: Only count trips when port changes trips = port_changes_in_batch + 2 # +2 for initial and return trips
Solution: Use the preprocessed port_changes
array which correctly marks transitions:
port_changes = [int(boxes[i][0] != boxes[i+1][0]) for i in range(n-1)]
4. Missing Edge Cases in Constraint Validation
Pitfall: Not handling the case where even a single box exceeds constraints, or when the deque becomes empty after removing invalid indices.
Solution: Always check if the deque is non-empty before accessing:
if deque_indices: # Essential check
dp[i] = changes_prefix_sum[i - 1] + dp[deque_indices[0]] -
changes_prefix_sum[deque_indices[0]] + 2
else:
# This shouldn't happen with valid inputs, but good to handle
dp[i] = float('inf') # Or handle the error appropriately
5. Inefficient Pairwise Implementation
Pitfall: For Python versions before 3.10, pairwise
isn't available in itertools, causing runtime errors.
Solution: Provide a fallback implementation or use manual iteration:
# Option 1: Manual iteration
port_changes = [int(boxes[i][0] != boxes[i+1][0]) for i in range(len(boxes)-1)]
# Option 2: Custom pairwise for older Python versions
def pairwise(iterable):
a, b = tee(iterable)
next(b, None)
return zip(a, b)
6. Integer Overflow in Weight Calculations
Pitfall: When dealing with large weight values and many boxes, prefix sums might overflow in languages with fixed integer sizes (though Python handles this automatically).
Solution: In Python this isn't an issue, but in other languages, use appropriate data types:
# For other languages, ensure using long/int64 for weight calculations weight_prefix_sum = [0] for box in boxes: weight_prefix_sum.append(weight_prefix_sum[-1] + box[1])
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
Segment Tree Faster Range Queries For this article we want to introduce the idea of a Segment Tree Segment Trees allow us to quickly perform range queries as well as range updates Suppose we have an array and we want to know the sum of a particular range of numbers as well
Queue Intro Think of the last time you stood in line to buy movie tickets The first person in line gets their ticket first and any newcomers join the end of the line This system where the first person to arrive is the first to be served is a queue in real
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!