Before attempting this problem, you should be comfortable with:
Greedy Algorithms - Understanding when local optimal choices lead to global optimal solutions
Heaps (Priority Queues) - Using min-heaps and max-heaps to efficiently track extreme values
Binary Search - Searching for optimal values in a sorted or monotonic space
Sorting - Ordering elements to make greedy decisions on largest/smallest values
1. Brute Force (Greedy)
Intuition
The key observation is that ladders are most valuable for the largest height differences, since a ladder covers any gap regardless of size while bricks are consumed proportionally. For each position we reach, we can check if it's achievable by sorting all the height jumps encountered so far and using ladders for the largest ones while using bricks for the rest.
Algorithm
Iterate through each building index i from 1 to n - 1.
If we have enough ladders to cover all jumps up to index i, we can definitely reach it.
Otherwise, collect all positive height differences (jumps) from the start to index i.
Sort the jumps in ascending order.
Use bricks for the smallest jumps (all jumps except the largest ladders ones).
If the total bricks needed exceeds what we have, return the previous index i - 1.
If we successfully iterate through all buildings, return n - 1.
publicclassSolution{publicintFurthestBuilding(int[] heights,int bricks,int ladders){int n = heights.Length;for(int i =1; i < n; i++){if(ladders >= i)continue;List<int> diffs =newList<int>();for(int j =0; j < i; j++){if(heights[j +1]> heights[j]){
diffs.Add(heights[j +1]- heights[j]);}}
diffs.Sort();long brickSum =0;for(int j =0; j < diffs.Count - ladders; j++){
brickSum += diffs[j];}if(brickSum > bricks)return i -1;}return n -1;}}
funcfurthestBuilding(heights []int, bricks int, ladders int)int{
n :=len(heights)for i :=1; i < n; i++{if ladders >= i {continue}
diffs :=[]int{}for j :=0; j < i; j++{if heights[j+1]> heights[j]{
diffs =append(diffs, heights[j+1]-heights[j])}}
sort.Ints(diffs)
brickSum :=0for j :=0; j <len(diffs)-ladders; j++{
brickSum += diffs[j]}if brickSum > bricks {return i -1}}return n -1}
class Solution {funfurthestBuilding(heights: IntArray, bricks: Int, ladders: Int): Int {val n = heights.size
for(i in1 until n){if(ladders >= i)continueval diffs = mutableListOf<Int>()for(j in0 until i){if(heights[j +1]> heights[j]){
diffs.add(heights[j +1]- heights[j])}}
diffs.sort()var brickSum =0Lfor(j in0 until diffs.size - ladders){
brickSum += diffs[j]}if(brickSum > bricks)return i -1}return n -1}}
classSolution{funcfurthestBuilding(_ heights:[Int],_ bricks:Int,_ ladders:Int)->Int{let n = heights.count
for i in1..<n {if ladders >= i {continue}var diffs =[Int]()for j in0..<i {if heights[j +1]> heights[j]{
diffs.append(heights[j +1]- heights[j])}}
diffs.sort()var brickSum =0for j in0..<(diffs.count - ladders){
brickSum += diffs[j]}if brickSum > bricks {return i -1}}return n -1}}
Time & Space Complexity
Time complexity: O(n2logn)
Space complexity: O(n)
2. Binary Search On Buildings
Intuition
Instead of checking each building sequentially, we can use binary search to find the furthest reachable building. The key insight is that reachability is monotonic: if we can reach building i, we can also reach all buildings before it. This lets us binary search on the answer.
Algorithm
Binary search on the building index with range [ladders - 1, n - 1].
For each mid index, check if we can reach it using canReach(mid).
In canReach: collect all positive height differences up to index mid.
Sort the differences and use bricks for the smallest ones (all except the largest ladders).
If total bricks needed is within budget, we can reach that building.
Adjust binary search bounds based on reachability.
classSolution:deffurthestBuilding(self, heights: List[int], bricks:int, ladders:int)->int:defcanReach(mid):
diffs =[]for i inrange(mid):if heights[i +1]> heights[i]:
diffs.append(heights[i +1]- heights[i])
diffs.sort()
brickSum =0for j inrange(len(diffs)- ladders):
brickSum += diffs[j]if brickSum > bricks:returnFalsereturnTrue
l, r = ladders -1,len(heights)-1while l <= r:
mid =(l + r)//2if canReach(mid):
l = mid +1else:
r = mid -1return l -1
publicclassSolution{publicintfurthestBuilding(int[] heights,int bricks,int ladders){int l = ladders -1, r = heights.length -1;while(l <= r){int mid =(l + r)/2;if(canReach(heights, mid, bricks, ladders)){
l = mid +1;}else{
r = mid -1;}}return l -1;}privatebooleancanReach(int[] heights,int mid,int bricks,int ladders){List<Integer> diffs =newArrayList<>();for(int i =0; i < mid; i++){if(heights[i +1]> heights[i]){
diffs.add(heights[i +1]- heights[i]);}}Collections.sort(diffs);int brickSum =0;for(int j =0; j < diffs.size()- ladders; j++){
brickSum += diffs.get(j);if(brickSum > bricks){returnfalse;}}returntrue;}}
classSolution{public:intfurthestBuilding(vector<int>& heights,int bricks,int ladders){int l = ladders -1, r = heights.size()-1;while(l <= r){int mid =(l + r)/2;if(canReach(heights, mid, bricks, ladders)){
l = mid +1;}else{
r = mid -1;}}return l -1;}private:boolcanReach(vector<int>& heights,int mid,int bricks,int ladders){
vector<int> diffs;for(int i =0; i < mid; i++){if(heights[i +1]> heights[i]){
diffs.push_back(heights[i +1]- heights[i]);}}sort(diffs.begin(), diffs.end());longlong brickSum =0;for(int j =0; j <int(diffs.size())- ladders; j++){
brickSum += diffs[j];if(brickSum > bricks){returnfalse;}}returntrue;}};
classSolution{/**
* @param {number[]} heights
* @param {number} bricks
* @param {number} ladders
* @return {number}
*/furthestBuilding(heights, bricks, ladders){let l = ladders -1,
r = heights.length -1;constcanReach=(mid)=>{let diffs =[];for(let i =0; i < mid; i++){if(heights[i +1]> heights[i]){
diffs.push(heights[i +1]- heights[i]);}}
diffs.sort((a, b)=> a - b);let brickSum =0;for(let j =0; j < diffs.length - ladders; j++){
brickSum += diffs[j];if(brickSum > bricks){returnfalse;}}returntrue;};while(l <= r){let mid = Math.floor((l + r)/2);if(canReach(mid)){
l = mid +1;}else{
r = mid -1;}}return l -1;}}
publicclassSolution{publicintFurthestBuilding(int[] heights,int bricks,int ladders){int l = ladders -1, r = heights.Length -1;while(l <= r){int mid =(l + r)/2;if(CanReach(heights, mid, bricks, ladders)){
l = mid +1;}else{
r = mid -1;}}return l -1;}privateboolCanReach(int[] heights,int mid,int bricks,int ladders){List<int> diffs =newList<int>();for(int i =0; i < mid; i++){if(heights[i +1]> heights[i]){
diffs.Add(heights[i +1]- heights[i]);}}
diffs.Sort();int brickSum =0;for(int j =0; j < diffs.Count - ladders; j++){
brickSum += diffs[j];if(brickSum > bricks)returnfalse;}returntrue;}}
funcfurthestBuilding(heights []int, bricks int, ladders int)int{
l, r := ladders-1,len(heights)-1
canReach :=func(mid int)bool{
diffs :=[]int{}for i :=0; i < mid; i++{if heights[i+1]> heights[i]{
diffs =append(diffs, heights[i+1]-heights[i])}}
sort.Ints(diffs)
brickSum :=0for j :=0; j <len(diffs)-ladders; j++{
brickSum += diffs[j]if brickSum > bricks {returnfalse}}returntrue}for l <= r {
mid :=(l + r)/2ifcanReach(mid){
l = mid +1}else{
r = mid -1}}return l -1}
class Solution {funfurthestBuilding(heights: IntArray, bricks: Int, ladders: Int): Int {var l = ladders -1var r = heights.size -1funcanReach(mid: Int): Boolean {val diffs = mutableListOf<Int>()for(i in0 until mid){if(heights[i +1]> heights[i]){
diffs.add(heights[i +1]- heights[i])}}
diffs.sort()var brickSum =0for(j in0 until diffs.size - ladders){
brickSum += diffs[j]if(brickSum > bricks)returnfalse}returntrue}while(l <= r){val mid =(l + r)/2if(canReach(mid)){
l = mid +1}else{
r = mid -1}}return l -1}}
classSolution{funcfurthestBuilding(_ heights:[Int],_ bricks:Int,_ ladders:Int)->Int{var l = ladders -1var r = heights.count -1funccanReach(_ mid:Int)->Bool{var diffs =[Int]()for i in0..<mid {if heights[i +1]> heights[i]{
diffs.append(heights[i +1]- heights[i])}}
diffs.sort()var brickSum =0for j in0..<(diffs.count - ladders){
brickSum += diffs[j]if brickSum > bricks {returnfalse}}returntrue}while l <= r {let mid =(l + r)/2ifcanReach(mid){
l = mid +1}else{
r = mid -1}}return l -1}}
Time & Space Complexity
Time complexity: O(nlog2n)
Space complexity: O(n)
3. Binary Search On Buildings (Optimal)
Intuition
We can optimize the previous approach by pre-sorting all height differences once instead of sorting repeatedly for each binary search check. By storing differences with their indices and sorting them in descending order, we can efficiently determine for any target building which jumps to cover with ladders (the largest ones) and which to cover with bricks.
Algorithm
Pre-compute all positive height differences along with their indices.
Sort these differences in descending order.
Binary search on building indices from 1 to n - 1.
For each mid index, iterate through sorted differences: use ladders for the largest jumps (up to ladders count), and use bricks for the rest that occur before or at the mid index.
If bricks exceed the budget, that building is unreachable.
publicclassSolution{publicintFurthestBuilding(int[] heights,int bricks,int ladders){var diffs =newList<int[]>();for(int i =1; i < heights.Length; i++){if(heights[i]> heights[i -1]){
diffs.Add(newint[]{ heights[i]- heights[i -1], i });}}
diffs.Sort((a, b)=> b[0].CompareTo(a[0]));int l =1, r = heights.Length -1;while(l <= r){int mid =(l + r)>>1;if(CanReach(diffs, mid, bricks, ladders)){
l = mid +1;}else{
r = mid -1;}}return l -1;}privateboolCanReach(List<int[]> diffs,int index,int bricks,int ladders){int useLadders =0;long useBricks =0;foreach(var diff in diffs){int jump = diff[0], i = diff[1];if(i > index)continue;if(useLadders < ladders){
useLadders++;}else{
useBricks += jump;if(useBricks > bricks)returnfalse;}}returntrue;}}
funcfurthestBuilding(heights []int, bricks int, ladders int)int{type pair struct{
diff, idx int}
diffs :=[]pair{}for i :=1; i <len(heights); i++{if heights[i]> heights[i-1]{
diffs =append(diffs, pair{heights[i]- heights[i-1], i})}}
sort.Slice(diffs,func(i, j int)bool{return diffs[i].diff > diffs[j].diff
})
canReach :=func(index int)bool{
useLadders, useBricks :=0,0for_, d :=range diffs {if d.idx > index {continue}if useLadders < ladders {
useLadders++}else{
useBricks += d.diff
if useBricks > bricks {returnfalse}}}returntrue}
l, r :=1,len(heights)-1for l <= r {
mid :=(l + r)>>1ifcanReach(mid){
l = mid +1}else{
r = mid -1}}return l -1}
class Solution {funfurthestBuilding(heights: IntArray, bricks: Int, ladders: Int): Int {val diffs = mutableListOf<IntArray>()for(i in1 until heights.size){if(heights[i]> heights[i -1]){
diffs.add(intArrayOf(heights[i]- heights[i -1], i))}}
diffs.sortByDescending{ it[0]}funcanReach(index: Int): Boolean {var useLadders =0var useBricks =0Lfor(diff in diffs){val(jump, i)= diff[0]to diff[1]if(i > index)continueif(useLadders < ladders){
useLadders++}else{
useBricks += jump
if(useBricks > bricks)returnfalse}}returntrue}var l =1var r = heights.size -1while(l <= r){val mid =(l + r)shr1if(canReach(mid)){
l = mid +1}else{
r = mid -1}}return l -1}}
classSolution{funcfurthestBuilding(_ heights:[Int],_ bricks:Int,_ ladders:Int)->Int{var diffs =[(Int,Int)]()for i in1..<heights.count {if heights[i]> heights[i -1]{
diffs.append((heights[i]- heights[i -1], i))}}
diffs.sort {$0.0>$1.0}funccanReach(_ index:Int)->Bool{var useLadders =0var useBricks =0for(diff, i)in diffs {if i > index {continue}if useLadders < ladders {
useLadders +=1}else{
useBricks += diff
if useBricks > bricks {returnfalse}}}returntrue}var l =1var r = heights.count -1while l <= r {let mid =(l + r)>>1ifcanReach(mid){
l = mid +1}else{
r = mid -1}}return l -1}}
Time & Space Complexity
Time complexity: O(nlogn)
Space complexity: O(n)
4. Max-Heap
Intuition
We can make greedy decisions as we traverse by initially using bricks for each jump, then retroactively swapping to a ladder when bricks run out. A max-heap tracks all brick usages so far, allowing us to efficiently swap the largest brick usage with a ladder when needed. This ensures ladders always cover the biggest gaps.
Algorithm
Traverse buildings from left to right.
For each positive height difference, subtract it from bricks and push the difference onto a max-heap.
If bricks go negative:
If no ladders remain, return the current index (cannot proceed).
Otherwise, use a ladder: pop the largest difference from the heap and add it back to bricks.
Instead of tracking brick usages, we can track ladder usages in a min-heap. We greedily assign ladders to each jump, but once we've used more than the allowed number of ladders, we convert the smallest ladder usage back to bricks. This way, ladders always end up covering the largest jumps.
Algorithm
Traverse buildings from left to right.
For each positive height difference, push it onto a min-heap (representing a ladder allocation).
If the heap size exceeds the number of ladders:
Pop the smallest difference (least valuable ladder usage).
Not all building transitions require resources. When heights[i+1] <= heights[i], no bricks or ladders are needed. Forgetting to skip these cases leads to wasting resources on non-existent climbs and incorrect answers. Always check if the height difference is positive before consuming any resources.
Using Ladders for Small Gaps Instead of Large Ones
The greedy insight is that ladders should cover the largest height differences since they handle any gap regardless of size. Using ladders greedily for the first gaps encountered (rather than the largest ones) leads to suboptimal resource usage. The heap-based solutions ensure ladders always end up covering the maximum height differences.
Returning the Wrong Index When Resources Run Out
When bricks become negative, the current building cannot be reached, so the answer should be the previous index. A common mistake is returning i instead of i - 1 in the brute force approach, or returning i when we should return i (the last reachable building) in the heap approach. Pay careful attention to what index represents the last successfully reached building.