LeetCode 1840 - Maximum Building Height
This problem asks us to construct heights for n buildings arranged in a straight line while satisfying several constraints. Each building has a non-negative integer height. Building 1 must always have height 0.
Difficulty: 🔴 Hard
Topics: Array, Math, Sorting
Solution
LeetCode 1840 - Maximum Building Height
Problem Understanding
This problem asks us to construct heights for n buildings arranged in a straight line while satisfying several constraints.
Each building has a non-negative integer height. Building 1 must always have height 0. Additionally, adjacent buildings cannot differ in height by more than 1. This means that if one building has height h, the next building can only have height h - 1, h, or h + 1, assuming heights remain non-negative.
On top of these global constraints, some buildings have explicit upper bounds. The array restrictions contains pairs [id, maxHeight], meaning building id cannot exceed maxHeight.
The goal is not to construct the full height array explicitly. Instead, we only need to determine the maximum possible height that any building can achieve while satisfying all constraints.
The input size is large:
ncan be as large as10^9restrictions.lengthcan be as large as10^5
The enormous value of n immediately tells us that we cannot simulate all buildings one by one. Any solution depending on iterating through every building position would be too slow.
The important observation is that only restricted buildings matter directly. Between restricted positions, the height changes are governed entirely by the slope constraint of at most 1 per step.
Several edge cases are important:
- There may be no restrictions at all. In that case, the tallest building is simply
n - 1, because heights can increase by1starting from building1. - Restrictions may be inconsistent unless adjusted. For example, if one building is forced very low, nearby restrictions may also need to be reduced.
- The last building may not appear in the restrictions array. We must still account for it.
- Restrictions can appear in arbitrary order, so sorting is required.
Approaches
Brute Force Approach
A brute force strategy would attempt to assign heights to every building while respecting all constraints.
One possibility would be:
- Initialize all buildings with their maximum feasible heights.
- Propagate constraints left and right repeatedly until all adjacent differences are at most
1. - Track the maximum resulting height.
This approach is correct because eventually all constraints become satisfied through repeated relaxation.
However, it is completely impractical for the given constraints. Since n can reach 10^9, even storing all building heights is impossible.
The brute force idea fails because it treats every building individually, while the real structure of the problem depends only on restriction boundaries.
Optimal Approach
The key insight is that only restricted buildings matter.
Suppose we know the maximum allowed heights at two positions:
(x1, h1)(x2, h2)
Because adjacent heights can differ by at most 1, the feasible height profile between them forms a "mountain" shape.
The tallest achievable point between these positions depends only on:
- the distance between them
- the endpoint height limits
This transforms the problem into constraint propagation between restricted positions.
The algorithm works in three phases:
- Add implicit restrictions:
- building
1has height0 - building
ncan initially be at mostn - 1
- Sort restrictions by position.
- Perform:
- a left-to-right pass
- a right-to-left pass
These passes tighten all restriction heights so they become mutually consistent.
After that, each neighboring restriction pair independently determines the tallest achievable peak between them.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n) or worse | O(n) | Impossible for large n |
| Optimal | O(m log m) | O(m) | m = restrictions.length, uses sorting and constraint propagation |
Algorithm Walkthrough
Step 1: Add implicit restrictions
Building 1 must always have height 0, so we add:
[1, 0]
If building n is not already restricted, we add:
[n, n - 1]
Why n - 1?
Starting from building 1 at height 0, the largest possible height increase over n - 1 steps is exactly n - 1.
This gives a natural upper bound for the last building.
Step 2: Sort restrictions
We sort all restrictions by building index.
This allows us to process neighboring restrictions efficiently.
Suppose after sorting we have:
[id1, h1], [id2, h2]
The distance between them is:
d = id2 - id1
Because heights can change by at most 1 per step:
h2 <= h1 + d
and similarly:
h1 <= h2 + d
These inequalities define feasible relationships between neighboring restrictions.
Step 3: Left-to-right constraint propagation
We scan from left to right.
For each restriction:
currentHeight = min(currentHeight, previousHeight + distance)
This ensures that the current restriction is reachable from the previous one.
Without this pass, some restrictions could demand impossible jumps upward.
Step 4: Right-to-left constraint propagation
We now scan from right to left.
For each restriction:
currentHeight = min(currentHeight, nextHeight + distance)
This ensures feasibility from the opposite direction.
Together, the two passes guarantee global consistency.
Step 5: Compute maximum peak between restrictions
Now consider two neighboring restrictions:
(x1, h1)
(x2, h2)
Let:
d = x2 - x1
The tallest possible peak occurs when we increase from the lower side as much as possible before descending toward the other endpoint.
The maximum achievable height is:
(h1 + h2 + d) // 2
This formula comes from balancing the ascent and descent slopes.
We compute this value for every adjacent pair and take the maximum.
Why it works
After the two propagation passes, every restriction becomes globally feasible. That means any adjacent restriction pair can be connected using valid slopes.
Between two feasible endpoints, the optimal strategy for maximizing height is always to climb as high as possible before descending. Because the slope limit is 1, the tallest achievable point is exactly the midpoint balancing formula:
(h1 + h2 + d) // 2
Taking the maximum over all neighboring intervals therefore gives the tallest achievable building overall.
Python Solution
from typing import List
class Solution:
def maxBuilding(self, n: int, restrictions: List[List[int]]) -> int:
restrictions.append([1, 0])
has_last = False
for building_id, _ in restrictions:
if building_id == n:
has_last = True
break
if not has_last:
restrictions.append([n, n - 1])
restrictions.sort()
# Left-to-right pass
for i in range(1, len(restrictions)):
prev_id, prev_height = restrictions[i - 1]
curr_id, curr_height = restrictions[i]
max_allowed = prev_height + (curr_id - prev_id)
restrictions[i][1] = min(curr_height, max_allowed)
# Right-to-left pass
for i in range(len(restrictions) - 2, -1, -1):
next_id, next_height = restrictions[i + 1]
curr_id, curr_height = restrictions[i]
max_allowed = next_height + (next_id - curr_id)
restrictions[i][1] = min(curr_height, max_allowed)
answer = 0
# Compute best peak between adjacent restrictions
for i in range(1, len(restrictions)):
left_id, left_height = restrictions[i - 1]
right_id, right_height = restrictions[i]
distance = right_id - left_id
peak = (left_height + right_height + distance) // 2
answer = max(answer, peak)
return answer
The implementation begins by inserting the mandatory restriction [1, 0].
Next, it ensures that building n has an explicit restriction. Even though the problem may not provide one, adding [n, n - 1] simplifies later processing because every interval now has a defined endpoint.
The restrictions are then sorted by building position.
The left-to-right pass enforces that each restriction can be reached from the previous restriction under the slope constraint. If a restriction is too high, it gets reduced.
The right-to-left pass performs the symmetric adjustment from the opposite direction.
At this point, all restrictions are globally feasible.
Finally, the algorithm examines each neighboring pair and computes the tallest achievable peak between them using the midpoint formula.
The maximum over all intervals is returned.
Go Solution
package main
import "sort"
func maxBuilding(n int, restrictions [][]int) int {
restrictions = append(restrictions, []int{1, 0})
hasLast := false
for _, r := range restrictions {
if r[0] == n {
hasLast = true
break
}
}
if !hasLast {
restrictions = append(restrictions, []int{n, n - 1})
}
sort.Slice(restrictions, func(i, j int) bool {
return restrictions[i][0] < restrictions[j][0]
})
// Left-to-right pass
for i := 1; i < len(restrictions); i++ {
prevID := restrictions[i-1][0]
prevHeight := restrictions[i-1][1]
currID := restrictions[i][0]
currHeight := restrictions[i][1]
maxAllowed := prevHeight + (currID - prevID)
if currHeight > maxAllowed {
restrictions[i][1] = maxAllowed
}
}
// Right-to-left pass
for i := len(restrictions) - 2; i >= 0; i-- {
nextID := restrictions[i+1][0]
nextHeight := restrictions[i+1][1]
currID := restrictions[i][0]
currHeight := restrictions[i][1]
maxAllowed := nextHeight + (nextID - currID)
if currHeight > maxAllowed {
restrictions[i][1] = maxAllowed
}
}
answer := 0
for i := 1; i < len(restrictions); i++ {
leftID := restrictions[i-1][0]
leftHeight := restrictions[i-1][1]
rightID := restrictions[i][0]
rightHeight := restrictions[i][1]
distance := rightID - leftID
peak := (leftHeight + rightHeight + distance) / 2
if peak > answer {
answer = peak
}
}
return answer
}
The Go implementation follows the exact same algorithmic structure as the Python version.
The primary difference is the use of sort.Slice for sorting the restrictions.
Go slices are modified in place, so updating restriction heights directly changes the underlying data structure without extra copying.
Integer overflow is not a concern because the maximum possible values remain comfortably within Go's 64-bit integer range on modern systems, and LeetCode's int type is sufficient for these constraints.
Worked Examples
Example 1
Input:
n = 5
restrictions = [[2,1],[4,1]]
Step 1: Add implicit restrictions
[[2,1],[4,1],[1,0],[5,4]]
Step 2: Sort
| Index | Restriction |
|---|---|
| 0 | [1,0] |
| 1 | [2,1] |
| 2 | [4,1] |
| 3 | [5,4] |
Step 3: Left-to-right pass
| Current | Previous | Max Allowed | Updated |
|---|---|---|---|
| [2,1] | [1,0] | 1 | [2,1] |
| [4,1] | [2,1] | 3 | [4,1] |
| [5,4] | [4,1] | 2 | [5,2] |
Result:
[[1,0],[2,1],[4,1],[5,2]]
Step 4: Right-to-left pass
No further reductions occur.
Step 5: Compute peaks
| Interval | Formula | Peak |
|---|---|---|
| [1,0] to [2,1] | (0 + 1 + 1) // 2 | 1 |
| [2,1] to [4,1] | (1 + 1 + 2) // 2 | 2 |
| [4,1] to [5,2] | (1 + 2 + 1) // 2 | 2 |
Answer:
2
Example 2
Input:
n = 6
restrictions = []
Step 1: Add implicit restrictions
[[1,0],[6,5]]
Step 2: Left-to-right pass
No reductions needed.
Step 3: Right-to-left pass
No reductions needed.
Step 4: Compute peak
| Interval | Formula | Peak |
|---|---|---|
| [1,0] to [6,5] | (0 + 5 + 5) // 2 | 5 |
Answer:
5
Example 3
Input:
n = 10
restrictions = [[5,3],[2,5],[7,4],[10,3]]
Step 1: Add implicit restriction
[[1,0],[2,5],[5,3],[7,4],[10,3]]
Step 2: Sort
| Index | Restriction |
|---|---|
| 0 | [1,0] |
| 1 | [2,5] |
| 2 | [5,3] |
| 3 | [7,4] |
| 4 | [10,3] |
Step 3: Left-to-right pass
| Restriction | Updated Height |
|---|---|
| [2,5] | 1 |
| [5,3] | 3 |
| [7,4] | 4 |
| [10,3] | 3 |
Result:
[[1,0],[2,1],[5,3],[7,4],[10,3]]
Step 4: Right-to-left pass
No further changes.
Step 5: Compute peaks
| Interval | Peak |
|---|---|
| [1,0] to [2,1] | 1 |
| [2,1] to [5,3] | 3 |
| [5,3] to [7,4] | 4 |
| [7,4] to [10,3] | 5 |
Answer:
5
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(m log m) | Sorting dominates the runtime |
| Space | O(m) | Stores all restrictions including added boundaries |
Here, m is the number of restrictions.
The two propagation passes and the final peak computation are all linear in the number of restrictions. The only non-linear operation is sorting.
The algorithm never depends on n directly, which is critical because n may be as large as 10^9.
Test Cases
from typing import List
class Solution:
def maxBuilding(self, n: int, restrictions: List[List[int]]) -> int:
restrictions.append([1, 0])
has_last = False
for building_id, _ in restrictions:
if building_id == n:
has_last = True
break
if not has_last:
restrictions.append([n, n - 1])
restrictions.sort()
for i in range(1, len(restrictions)):
prev_id, prev_height = restrictions[i - 1]
curr_id, curr_height = restrictions[i]
restrictions[i][1] = min(
curr_height,
prev_height + (curr_id - prev_id)
)
for i in range(len(restrictions) - 2, -1, -1):
next_id, next_height = restrictions[i + 1]
curr_id, curr_height = restrictions[i]
restrictions[i][1] = min(
curr_height,
next_height + (next_id - curr_id)
)
answer = 0
for i in range(1, len(restrictions)):
left_id, left_height = restrictions[i - 1]
right_id, right_height = restrictions[i]
distance = right_id - left_id
peak = (left_height + right_height + distance) // 2
answer = max(answer, peak)
return answer
sol = Solution()
assert sol.maxBuilding(5, [[2,1],[4,1]]) == 2
# Basic example with two restrictions
assert sol.maxBuilding(6, []) == 5
# No restrictions, pure increasing sequence
assert sol.maxBuilding(10, [[5,3],[2,5],[7,4],[10,3]]) == 5
# Example with multiple interacting restrictions
assert sol.maxBuilding(2, []) == 1
# Smallest valid n
assert sol.maxBuilding(5, [[5,0]]) == 2
# Last building forced low
assert sol.maxBuilding(10, [[2,1]]) == 9
# Early restriction does not prevent later growth
assert sol.maxBuilding(10, [[2,0]]) == 7
# Immediate flattening then growth
assert sol.maxBuilding(10, [[3,0],[8,0]]) == 2
# Valley between strict zero-height restrictions
assert sol.maxBuilding(1000000000, []) == 999999999
# Maximum n without restrictions
assert sol.maxBuilding(10, [[4,2],[7,2]]) == 4
# Symmetric restriction interval
| Test | Why |
|---|---|
n=5, [[2,1],[4,1]] |
Validates standard restricted growth |
n=6, [] |
Validates unrestricted increasing heights |
n=10, [[5,3],[2,5],[7,4],[10,3]] |
Tests propagation in both directions |
n=2, [] |
Smallest possible input |
n=5, [[5,0]] |
Ensures low final restriction propagates backward |
n=10, [[2,1]] |
Tests large unrestricted suffix |
n=10, [[2,0]] |
Tests forced flat start |
n=10, [[3,0],[8,0]] |
Tests narrow feasible mountain |
n=1000000000, [] |
Confirms algorithm does not depend on n |
n=10, [[4,2],[7,2]] |
Tests balanced interval peak calculation |
Edge Cases
One important edge case occurs when there are no restrictions at all. A naive implementation might incorrectly return 0 or fail because no intervals exist. The correct behavior is that heights can increase by 1 starting from building 1, producing a tallest height of n - 1. The implementation handles this by explicitly adding [1,0] and [n,n-1].
Another tricky case happens when restrictions are initially inconsistent. For example:
[[2,0],[5,10]]
The restriction at building 5 cannot actually reach height 10 because there are only three steps between buildings 2 and 5. The left-to-right propagation pass automatically reduces impossible heights to their maximum feasible values.
A third important edge case occurs when the tallest point lies between restrictions rather than directly on one. Many incorrect solutions only examine restricted buildings themselves. However, the actual maximum may appear in the middle of an interval where the height rises and then falls. The midpoint formula correctly computes this hidden peak.
Another subtle case is when the final building already appears in the restriction list. The implementation avoids adding a duplicate restriction for building n, preventing incorrect interval calculations or duplicated processing.