Time to Cross a Bridge
HardPrompt
There are k workers who want to move n boxes from the right (old) warehouse to the left (new) warehouse. You are given the two integers n and k, and a 2D integer array time of size k x 4 where time[i] = [righti, picki, lefti, puti].
The warehouses are separated by a river and connected by a bridge. Initially, all k workers are waiting on the left side of the bridge. To move the boxes, the ith worker can do the following:
- Cross the bridge to the right side in
rightiminutes. - Pick a box from the right warehouse in
pickiminutes. - Cross the bridge to the left side in
leftiminutes. - Put the box into the left warehouse in
putiminutes.
The ith worker is less efficient than the jth worker if either condition is met:
lefti + righti > leftj + rightjlefti + righti == leftj + rightjandi > j
The following rules regulate the movement of the workers through the bridge:
- Only one worker can use the bridge at a time.
- When the bridge is unused prioritize the least efficient worker (who have picked up the box) on the right side to cross. If not, prioritize the least efficient worker on the left side to cross.
- If enough workers have already been dispatched from the left side to pick up all the remaining boxes, no more workers will be sent from the left side.
Return the elapsed minutes at which the last box reaches the left side of the bridge.
Example 1:
Input: n = 1, k = 3, time = [[1,1,2,1],[1,1,3,1],[1,1,4,1]]
Output: 6
Explanation:
From 0 to 1 minutes: worker 2 crosses the bridge to the right.
From 1 to 2 minutes: worker 2 picks up a box from the right warehouse.
From 2 to 6 minutes: worker 2 crosses the bridge to the left.
From 6 to 7 minutes: worker 2 puts a box at the left warehouse.
The whole process ends after 7 minutes. We return 6 because the problem asks for the instance of time at which the last worker reaches the left side of the bridge.Example 2:
Input: n = 3, k = 2, time = [[1,5,1,8],[10,10,10,10]]
Output: 37
Explanation:
The last box reaches the left side at 37 seconds. Notice, how we do not put the last boxes down, as that would take more time, and they are already on the left with the workers.
Constraints:
1 <= n, k <= 104time.length == ktime[i].length == 41 <= lefti, picki, righti, puti <= 1000
Approaches
2 approaches with complexity analysis and trade-offs.
This approach simulates the process by incrementing time one unit at a time. At each time step, it checks the status of all workers and the bridge to see if any state changes occur. This method is straightforward to conceptualize but is computationally expensive.
Algorithm
- Initialize
currentTime = 0, worker states (location, task, finish time),boxes_to_move = n,boxes_delivered = 0. - Create lists or arrays to represent workers waiting on the left and right sides.
- Start a loop that continues as long as
boxes_delivered < n. - Inside the loop, for the current
currentTime: a. Iterate through allkworkers. If a worker's task was scheduled to finish atcurrentTime, update their status (e.g., from 'picking' to 'waiting on right'). b. Check if the bridge is free (i.e.,currentTimeis greater than or equal to the time the last crossing ends). c. If the bridge is free, determine who crosses next: i. Check the list of workers waiting on the right. If not empty, find the one with the highest priority (least efficient) and dispatch them. ii. If no one from the right crosses, check the list of workers waiting on the left. If not empty and more boxes need to be moved, find the highest priority worker and dispatch them. iii. When a worker is dispatched, update their state to 'crossing', calculate their arrival time, and set the bridge's busy-until time. d. IncrementcurrentTimeby 1. - The simulation ends when the
n-th box reaches the left side of the bridge. Return thecurrentTimeof that event.
Walkthrough
In this method, we maintain the state of each worker, including their location (left/right), their current task (waiting, crossing, picking, putting), and the time they will finish their current task. We also track the state of the bridge (busy/free) and until when it's busy. The simulation proceeds in a loop, incrementing a currentTime variable from 0 upwards. In each iteration, we check all workers to see if they have completed their tasks. If the bridge is free, we apply the given priority rules to select a worker from the waiting queues (first from the right, then from the left). The selected worker begins crossing, and their state is updated accordingly. This continues until n boxes have reached the left side of the bridge.
Complexity
Time
O(T * k * log k), where `T` is the final completion time. `T` can be very large (e.g., `n` times the average cycle time). At each time step, we might need to iterate through and sort the `k` workers, making this approach too slow for the given constraints.
Space
O(k), to store the state of each of the `k` workers.
Trade-offs
Pros
Conceptually simple and easy to understand.
Cons
Extremely inefficient for the given constraints. The total simulation time can be very large, leading to a 'Time Limit Exceeded' error.
The complexity is proportional to the total elapsed time, which is not ideal for problems with large time values.
This optimized approach avoids simulating every single time unit by jumping from one significant "event" to the next. An event is a moment when a worker finishes a task (picking/putting) and becomes available, or when a crossing is completed. This allows the simulation to skip over idle time efficiently, making it suitable for the given constraints.
Algorithm
-
Initialization:
currentTime = 0.- Four priority queues:
waitL,waitR(max-heaps for waiting workers, prioritized by inefficiency),workL,workR(min-heaps for busy workers, prioritized by finish time). - Add all
kworkers towaitL. boxesToDispatch = n(boxes to be picked up from the right).
-
Simulation Loop: Continue as long as there are boxes to be moved or workers on the right side.
-
Advance Time and Update States:
- At the start of each loop, move any workers from
workLandworkRwhose tasks are finished (i.e.,finishTime <= currentTime) to their respective waiting queues (waitLorwaitR).
- At the start of each loop, move any workers from
-
Bridge Logic:
- Priority Right: If
waitRis not empty, poll the highest-priority worker. UpdatecurrentTimeby adding their left-crossing time. This newcurrentTimeis when they reach the left bank. If this is the last box, returncurrentTime. Otherwise, add the worker toworkLwith their 'put down' finish time. - Priority Left: If
waitRis empty butwaitLis not, and there are still boxes to dispatch (boxesToDispatch > 0), poll the highest-priority worker fromwaitL. UpdatecurrentTimeby adding their right-crossing time. Add the worker toworkRwith their 'pick up' finish time and decrementboxesToDispatch. - Idle Bridge: If no worker can cross (e.g., no one is waiting, or all dispatchable workers are on the left but no more boxes are needed), the system is idle. Advance
currentTimeto the earliest finish time of any worker inworkLorworkR.
- Priority Right: If
-
Termination: The loop terminates when the
n-th box reaches the left side of the bridge, and thatcurrentTimeis returned.
Walkthrough
We use four priority queues to manage the workers:
waitLandwaitR: Max-priority queues for workers waiting on the left and right sides, respectively. The priority is determined by the "least efficient" rule: higherleft_i + right_isum, with ties broken by higher indexi.workLandworkR: Min-priority queues for workers busy putting down a box (left) or picking one up (right). They store pairs of(finishTime, workerIndex)and are ordered by the earliestfinishTime.
The simulation revolves around a currentTime variable, which represents the time the bridge becomes free after a crossing. Instead of incrementing by one, currentTime is advanced based on crossing durations or jumps to the next time a worker becomes free. The simulation loop continues until all n boxes have been delivered. This event-driven model is highly efficient because it only processes moments where the system's state changes.
import java.util.*; class Solution { public int findCrossingTime(int n, int k, int[][] time) { // Max-heap for waiting workers based on efficiency Comparator<Integer> efficiencyComparator = (i, j) -> { int efficiency_i = time[i][0] + time[i][2]; int efficiency_j = time[j][0] + time[j][2]; if (efficiency_i != efficiency_j) { return efficiency_j - efficiency_i; // Higher sum is less efficient } return j - i; // Higher index is less efficient }; PriorityQueue<Integer> waitL = new PriorityQueue<>(efficiencyComparator); PriorityQueue<Integer> waitR = new PriorityQueue<>(efficiencyComparator); // Min-heap for busy workers, ordered by finish time PriorityQueue<long[]> workL = new PriorityQueue<>(Comparator.comparingLong(a -> a[0])); PriorityQueue<long[]> workR = new PriorityQueue<>(Comparator.comparingLong(a -> a[0])); for (int i = 0; i < k; i++) { waitL.add(i); } long currentTime = 0; int boxesToDispatch = n; // Loop until all boxes are dispatched and all workers are back on the left. // A simpler condition is to just track n boxes delivered. while (n > 0) { // Move workers who finished their tasks by currentTime while (!workL.isEmpty() && workL.peek()[0] <= currentTime) { waitL.add((int) workL.poll()[1]); } while (!workR.isEmpty() && workR.peek()[0] <= currentTime) { waitR.add((int) workR.poll()[1]); } // Bridge logic: prioritize right side if (!waitR.isEmpty()) { int workerIdx = waitR.poll(); currentTime += time[workerIdx][2]; // Cross to left n--; if (n == 0) { return (int) currentTime; } workL.add(new long[]{currentTime + time[workerIdx][3], workerIdx}); } // Then left side, if there are boxes to pick up else if (!waitL.isEmpty() && boxesToDispatch > 0) { int workerIdx = waitL.poll(); currentTime += time[workerIdx][0]; // Cross to right workR.add(new long[]{currentTime + time[workerIdx][1], workerIdx}); boxesToDispatch--; } // If bridge is idle, advance time to the next event else { long nextEventTime = Long.MAX_VALUE; if (!workL.isEmpty()) { nextEventTime = Math.min(nextEventTime, workL.peek()[0]); } if (!workR.isEmpty()) { nextEventTime = Math.min(nextEventTime, workR.peek()[0]); } currentTime = Math.max(currentTime, nextEventTime); } } return (int) currentTime; }}Complexity
Time
O(n * log k). Each of the `n` boxes requires a round trip. Each part of the trip (crossing, picking, putting) involves a few priority queue operations (add/poll), which take `O(log k)` time. The total number of such operations is proportional to `n`.
Space
O(k). The four priority queues, in total, store at most `k` workers at any given time.
Trade-offs
Pros
Highly efficient as it skips idle time by jumping between events.
Correctly models all the problem's constraints and priority rules.
Scales well with the given input constraints.
Cons
More complex to implement compared to a time-stepped simulation.
Requires careful management of state and time using multiple priority queues.
Solutions
Solution
class Solution {public int findCrossingTime(int n, int k, int[][] time) { int[][] t = new int[k][5]; for (int i = 0; i < k; ++i) { int[] x = time[i]; t[i] = new int[]{x[0], x[1], x[2], x[3], i}; } Arrays.sort( t, (a, b)->{ int x = a[0] + a[2], y = b[0] + b[2]; return x == y ? a[4] - b[4] : x - y; }); int cur = 0; PriorityQueue<Integer> waitInLeft = new PriorityQueue<>((a, b)->b - a); PriorityQueue<Integer> waitInRight = new PriorityQueue<>((a, b)->b - a); PriorityQueue<int[]> workInLeft = new PriorityQueue<>((a, b)->a[0] - b[0]); PriorityQueue<int[]> workInRight = new PriorityQueue<>((a, b)->a[0] - b[0]); for (int i = 0; i < k; ++i) { waitInLeft.offer(i); } while (true) { while (!workInLeft.isEmpty()) { int[] p = workInLeft.peek(); if (p[0] > cur) { break; } waitInLeft.offer(workInLeft.poll()[1]); } while (!workInRight.isEmpty()) { int[] p = workInRight.peek(); if (p[0] > cur) { break; } waitInRight.offer(workInRight.poll()[1]); } boolean leftToGo = n > 0 && !waitInLeft.isEmpty(); boolean rightToGo = !waitInRight.isEmpty(); if (!leftToGo && !rightToGo) { int nxt = 1 << 30; if (!workInLeft.isEmpty()) { nxt = Math.min(nxt, workInLeft.peek()[0]); } if (!workInRight.isEmpty()) { nxt = Math.min(nxt, workInRight.peek()[0]); } cur = nxt; continue; } if (rightToGo) { int i = waitInRight.poll(); cur += t[i][2]; if (n == 0 && waitInRight.isEmpty() && workInRight.isEmpty()) { return cur; } workInLeft.offer(new int[]{cur + t[i][3], i}); } else { int i = waitInLeft.poll(); cur += t[i][0]; --n; workInRight.offer(new int[]{cur + t[i][1], i}); } } }}class Solution {public: int findCrossingTime(int n, int k, vector<vector<int>> &time) { using pii = pair<int, int>; for (int i = 0; i < k; ++i) { time[i].push_back(i); } sort(time.begin(), time.end(), [](auto &a, auto &b) { int x = a[0] + a[2], y = b[0] + b[2]; return x == y ? a[4] < b[4] : x < y; }); int cur = 0; priority_queue<int> waitInLeft, waitInRight; priority_queue<pii, vector<pii>, greater<pii>> workInLeft, workInRight; for (int i = 0; i < k; ++i) { waitInLeft.push(i); } while (true) { while (!workInLeft.empty()) { auto [t, i] = workInLeft.top(); if (t > cur) { break; } workInLeft.pop(); waitInLeft.push(i); } while (!workInRight.empty()) { auto [t, i] = workInRight.top(); if (t > cur) { break; } workInRight.pop(); waitInRight.push(i); } bool leftToGo = n > 0 && !waitInLeft.empty(); bool rightToGo = !waitInRight.empty(); if (!leftToGo && !rightToGo) { int nxt = 1 << 30; if (!workInLeft.empty()) { nxt = min(nxt, workInLeft.top().first); } if (!workInRight.empty()) { nxt = min(nxt, workInRight.top().first); } cur = nxt; continue; } if (rightToGo) { int i = waitInRight.top(); waitInRight.pop(); cur += time[i][2]; if (n == 0 && waitInRight.empty() && workInRight.empty()) { return cur; } workInLeft.push({cur + time[i][3], i}); } else { int i = waitInLeft.top(); waitInLeft.pop(); cur += time[i][0]; --n; workInRight.push({cur + time[i][1], i}); } } }};class Solution: def findCrossingTime(self, n: int, k: int, time: List[List[int]]) -> int: time . sort(key=lambda x: x[0] + x[2]) cur = 0 wait_in_left, wait_in_right = [], [] work_in_left, work_in_right = [], [] for i in range(k): heappush(wait_in_left, - i) while 1: while work_in_left: t, i = work_in_left[0] if t > cur: break heappop(work_in_left) heappush(wait_in_left, - i) while work_in_right: t, i = work_in_right[0] if t > cur: break heappop(work_in_right) heappush(wait_in_right, - i) left_to_go = n > 0 and wait_in_left right_to_go = bool(wait_in_right) if not left_to_go and not right_to_go: nxt = inf if work_in_left: nxt = min(nxt, work_in_left[0][0]) if work_in_right: nxt = min(nxt, work_in_right[0][0]) cur = nxt continue if right_to_go: i = - heappop(wait_in_right) cur += time[i][2] if n == 0 and not wait_in_right and not work_in_right: return cur heappush(work_in_left, (cur + time[i][3], i)) else: i = - heappop(wait_in_left) cur += time[i][0] n -= 1 heappush(work_in_right, (cur + time[i][1], i))Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.