Furthest Building You Can Reach
MedPrompt
You are given an integer array heights representing the heights of buildings, some bricks, and some ladders.
You start your journey from building 0 and move to the next building by possibly using bricks or ladders.
While moving from building i to building i+1 (0-indexed),
- If the current building's height is greater than or equal to the next building's height, you do not need a ladder or bricks.
- If the current building's height is less than the next building's height, you can either use one ladder or
(h[i+1] - h[i])bricks.
Return the furthest building index (0-indexed) you can reach if you use the given ladders and bricks optimally.
Example 1:
Input: heights = [4,2,7,6,9,14,12], bricks = 5, ladders = 1
Output: 4
Explanation: Starting at building 0, you can follow these steps:
- Go to building 1 without using ladders nor bricks since 4 >= 2.
- Go to building 2 using 5 bricks. You must use either bricks or ladders because 2 < 7.
- Go to building 3 without using ladders nor bricks since 7 >= 6.
- Go to building 4 using your only ladder. You must use either bricks or ladders because 6 < 9.
It is impossible to go beyond building 4 because you do not have any more bricks or ladders.Example 2:
Input: heights = [4,12,2,7,3,18,20,3,19], bricks = 10, ladders = 2
Output: 7Example 3:
Input: heights = [14,3,19,3], bricks = 17, ladders = 0
Output: 3
Constraints:
1 <= heights.length <= 1051 <= heights[i] <= 1060 <= bricks <= 1090 <= ladders <= heights.length
Approaches
3 approaches with complexity analysis and trade-offs.
This approach reformulates the problem from "find the furthest building" to "can we reach building k?". Since the ability to reach a building is monotonic (if you can reach building k, you can reach all buildings before it), we can binary search for the furthest reachable index. For each candidate index k, we check if it's possible to reach it by calculating the resources needed. To do this, we find all the necessary climbs up to k, use ladders for the largest ones, and check if we have enough bricks for the rest.
Algorithm
- The problem has a monotonic property: if we can reach building
k, we can also reach any buildingj < k. This allows us to use binary search on the answer, which is the furthest reachable building index. - We binary search for an index
kwithin the range[0, n-1]. - For each
midindex from the binary search, we check if it's possible to reach that building using a helper functionisReachable(k). - The
isReachable(k)function works as follows:- Iterate from building
0tok-1and collect all the height differences (diff = heights[i+1] - heights[i]) for climbs (diff > 0) into a list. - To use resources optimally, ladders should be used for the largest climbs. Sort the list of differences in descending order.
- Use the available
laddersfor the largest differences. These climbs cost 0 bricks. - Sum the remaining smaller differences. This sum represents the total bricks required.
- If the required bricks are less than or equal to the available
bricks, then buildingkis reachable.
- Iterate from building
- Based on the result of
isReachable(mid):- If
true, it means we can reachmid, so we store it as a potential answer and try to reach further by settinglow = mid + 1. - If
false,midis unreachable, so we must try a smaller index by settinghigh = mid - 1.
- If
- The final answer is the largest index for which
isReachablereturned true.
Walkthrough
We can binary search on the building index to find the furthest one we can reach. The search space for the answer will be from 0 to n-1 (where n is the number of buildings).
For a given index k, we need a function isReachable(k) to determine if it's possible to get there. This function will gather all the climbs (where heights[i+1] > heights[i]) required to get from building 0 to k. To check if these climbs are possible, we should use our ladders on the largest climbs to save the most bricks. So, we collect all climb heights, sort them, assign ladders to the largest ones, and sum up the rest to see if our bricks are sufficient.
The main binary search loop will adjust its search range (low and high pointers) based on whether the middle index mid is reachable. If isReachable(mid) is true, we know we can at least reach mid, so we record it as our answer and try for a further building (low = mid + 1). If it's false, we know mid is too far, so we must aim for a closer building (high = mid - 1).
class Solution { public int furthestBuilding(int[] heights, int bricks, int ladders) { int n = heights.length; int low = 0, high = n - 1; int ans = 0; while (low <= high) { int mid = low + (high - low) / 2; if (isReachable(mid, heights, bricks, ladders)) { ans = mid; low = mid + 1; } else { high = mid - 1; } } return ans; } private boolean isReachable(int k, int[] heights, int bricks, int ladders) { java.util.List<Integer> climbs = new java.util.ArrayList<>(); for (int i = 0; i < k; i++) { if (heights[i+1] > heights[i]) { climbs.add(heights[i+1] - heights[i]); } } if (climbs.size() <= ladders) { return true; } java.util.Collections.sort(climbs, java.util.Collections.reverseOrder()); long bricksNeeded = 0; for (int i = ladders; i < climbs.size(); i++) { bricksNeeded += climbs.get(i); } return bricksNeeded <= bricks; }}Complexity
Time
O(N log N * log N). The binary search performs `O(log N)` iterations. In each iteration, `isReachable(k)` takes `O(k log k)` time to collect and sort up to `k` climbs. Since `k` can be up to `N`, this gives a total complexity of `O(N log^2 N)`. This can be optimized to `O(N log N)` by using a selection algorithm (like Quickselect) to find the `ladders`-th largest climb in `O(k)` time, but the sorting-based implementation is simpler.
Space
O(N), as the list of climbs can, in the worst case, store up to `N-1` differences.
Trade-offs
Pros
A standard and reliable pattern for problems with a monotonic property.
The logic is relatively easy to understand and implement correctly.
Cons
Less efficient than greedy approaches due to repeated computations. The
isReachablefunction recalculates and sorts climbs for each tested index.The complexity
O(N log^2 N)might be too slow for very large inputs, although it often passes within time limits.
A more direct greedy approach involves iterating through the buildings and making decisions on the fly. The strategy is to always try using the more abundant resource (bricks) first. We use bricks for every climb we encounter. To keep our options open, we store the cost of each of these climbs in a max-heap. If we ever run out of bricks, we check if we have any ladders. If so, we can 'undo' our most expensive brick purchase by using a ladder instead. The max-heap makes it easy to find this most expensive purchase. We take the largest climb from the heap, add its cost back to our brick supply, and use up one ladder. If we run out of bricks and have no ladders to salvage the situation, we've reached the furthest possible building.
Algorithm
- Iterate through the buildings from
i = 0ton-2. - At each step, calculate the height difference
diff = heights[i+1] - heights[i]. - If
diff <= 0, we can move to the next building for free. - If
diff > 0, we have a climb. We greedily assume we will use bricks for this climb.- We subtract
difffrom ourbrickscount. - We push the value of
diffinto a max-priority queue. This queue keeps track of all the brick amounts we have spent.
- We subtract
- After spending bricks, if our
brickscount becomes negative, it means we have a deficit.- To resolve the deficit, we must retroactively use a ladder on a previous climb.
- If we have no ladders left (
ladders == 0), we cannot resolve the deficit and are stuck. We return the current indexi. - If we have ladders, we should use one on the climb that required the most bricks to get the biggest "refund". This is the largest element in our max-priority queue.
- We pop the max element from the heap, add it back to our
brickscount, and decrementladders.
- If the loop completes without getting stuck, it means we can reach the final building. Return
n-1.
Walkthrough
This greedy approach iterates through the buildings once, making locally optimal decisions. We prioritize using bricks for any climb and only resort to using a ladder when we're forced to.
We use a max-priority queue to keep track of the height differences of all climbs for which we've used bricks. As we iterate from building i to i+1:
- If it's a climb (
heights[i+1] > heights[i]), we subtract the required bricks and add the difference to our max-heap. - If our brick count drops below zero, we're in debt. We must now use a ladder. If we have no ladders, we can't proceed, so
iis the furthest we can reach. - If we do have a ladder, we use it on the largest climb we've paid for with bricks so far. This is the most efficient way to reclaim bricks. We pop the top element from the max-heap (the largest climb), add it back to our
brickstotal, and decrement our ladder count. - We continue this until we either reach the end of the buildings or cannot proceed further.
class Solution { public int furthestBuilding(int[] heights, int bricks, int ladders) { // Max-heap to store the differences where we used bricks java.util.PriorityQueue<Integer> maxHeap = new java.util.PriorityQueue<>(java.util.Collections.reverseOrder()); for (int i = 0; i < heights.length - 1; i++) { int diff = heights[i+1] - heights[i]; if (diff > 0) { bricks -= diff; maxHeap.add(diff); // If we are out of bricks, we need to use a ladder on a past climb if (bricks < 0) { if (ladders > 0) { // Use a ladder on the largest climb to save the most bricks bricks += maxHeap.poll(); ladders--; } else { // No ladders left and not enough bricks return i; } } } } // If we reach here, we can get to the last building return heights.length - 1; }}Complexity
Time
O(N log N). We iterate through `N-1` buildings. In each iteration, we might perform a heap insertion and/or a poll operation. The heap can grow up to size `N`, so each heap operation takes `O(log N)` time.
Space
O(N) in the worst case, as the max-heap could potentially store a difference for every step if all are climbs.
Trade-offs
Pros
A single-pass
O(N log N)solution, which is generally more efficient than the binary search approach.The logic of "use bricks, then swap for a ladder if needed" can be quite intuitive.
Cons
The space complexity can be up to
O(N)if every step is a climb, which is less optimal than the min-heap approach.The time complexity of
O(N log N)is generally good but can be improved upon.
This is the most efficient approach, leveraging a greedy strategy with a min-heap. The insight is that ladders should always be used for the largest height differences to maximize their value. We iterate through the buildings, and for every climb, we add its height difference to a min-heap of a fixed capacity equal to the number of ladders. This heap effectively keeps track of the largest climbs seen so far. If the heap size exceeds the number of ladders, it means we have more climbs than ladders. In this case, the smallest climb in the heap (the one at the top) must be handled by bricks. We remove it from the heap and subtract its cost from our brick supply. If we can't afford it, we've found the furthest building we can reach.
Algorithm
- The core idea is that ladders are invaluable and should be reserved for the largest climbs.
- We iterate through the buildings from
i = 0ton-2. - We use a min-priority queue to keep track of the
ladderslargest climbs encountered so far. - At each step
i, calculate the height differencediff = heights[i+1] - heights[i]. - If
diff <= 0, move on for free. - If
diff > 0, we have a climb. We tentatively add thisdiffto our min-heap. This heap represents the climbs we are currently using ladders for. - If the size of the min-heap becomes greater than the number of
ladderswe have, it means we are trying to use one too many ladders. - To fix this, we must pay for one of the climbs with bricks. To be optimal, we should use bricks for the smallest of the climbs currently designated for ladders. This smallest climb is the root of the min-heap.
- We poll the minimum element from the heap and subtract this cost from our
brickscount. - If at any point our
brickscount becomes negative, it means we cannot afford even the smallest of the largest climbs. We are stuck and must return the current indexi. - If the loop finishes, we can reach the last building, so we return
n-1.
Walkthrough
This optimized greedy approach ensures that ladders are always used for the largest climbs encountered. We use a min-priority queue to maintain the k largest climbs, where k is the number of ladders.
As we iterate through the buildings:
- For each climb
diff > 0, we add it to the min-heap. - The min-heap will store the climbs we are reserving our ladders for.
- If the number of climbs in the heap exceeds the number of ladders, it means we can't use a ladder for all of them. The one to give up is the smallest one in this set, which is conveniently at the top of the min-heap.
- We
poll()this smallest climb from the heap and pay for it using bricks. - If our brick supply
bricksdrops below zero after paying, it's impossible to proceed. The furthest we can reach is the current buildingi. - If we successfully iterate through all the buildings, it means we can reach the end.
This method is highly efficient because the heap size is bounded by the number of ladders, which can be much smaller than N.
class Solution { public int furthestBuilding(int[] heights, int bricks, int ladders) { // Min-heap to store the `ladders` largest climbs java.util.PriorityQueue<Integer> minHeap = new java.util.PriorityQueue<>(); for (int i = 0; i < heights.length - 1; i++) { int diff = heights[i+1] - heights[i]; if (diff > 0) { minHeap.add(diff); // If we have more climbs than ladders, the smallest one must be paid by bricks if (minHeap.size() > ladders) { bricks -= minHeap.poll(); } // If we run out of bricks, we can't go further if (bricks < 0) { return i; } } } return heights.length - 1; }}Complexity
Time
O(N log L), where `N` is the number of buildings and `L` is the number of ladders. We iterate through `N-1` buildings, and each heap operation takes `O(log L)` time since the heap's size is capped by `L`.
Space
O(L), where `L` is the number of ladders. The min-heap stores at most `L+1` elements.
Trade-offs
Pros
The most efficient solution with the best time and space complexity.
The heap size is bounded by
ladders, making it very fast when the number of ladders is small.It's a single-pass algorithm.
Cons
The logic can be slightly less direct to grasp compared to the max-heap approach, as it involves maintaining a set of climbs for ladders and only paying with bricks when that set overflows.
Solutions
Solution
class Solution {public int furthestBuilding(int[] heights, int bricks, int ladders) { PriorityQueue<Integer> q = new PriorityQueue<>(); int n = heights.length; for (int i = 0; i < n - 1; ++i) { int a = heights[i], b = heights[i + 1]; int d = b - a; if (d > 0) { q.offer(d); if (q.size() > ladders) { bricks -= q.poll(); if (bricks < 0) { return i; } } } } return n - 1; }}class Solution {public: int furthestBuilding(vector<int> &heights, int bricks, int ladders) { priority_queue<int, vector<int>, greater<int>> q; int n = heights.size(); for (int i = 0; i < n - 1; ++i) { int a = heights[i], b = heights[i + 1]; int d = b - a; if (d > 0) { q.push(d); if (q.size() > ladders) { bricks -= q.top(); q.pop(); if (bricks < 0) { return i; } } } } return n - 1; }};class Solution: def furthestBuilding(self, heights: List[int], bricks: int, ladders: int) -> int: h = [] for i, a in enumerate(heights[: - 1]): b = heights[i + 1] d = b - a if d > 0: heappush(h, d) if len(h) > ladders: bricks -= heappop(h) if bricks < 0: return i return len(heights) - 1Video 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.