2015. Average Height of Buildings in Each Segment 🔒
Problem Description
You are given a street represented as a number line with buildings on it. Each building is described by a 2D array buildings
, where buildings[i] = [start_i, end_i, height_i]
means there's a building with height height_i
occupying the half-closed interval [start_i, end_i)
.
Your task is to describe the heights of all buildings on the street using the minimum number of non-overlapping segments. Each segment should represent a continuous portion of the street where buildings exist, along with their average height.
The output should be a 2D array street
where each element street[j] = [left_j, right_j, average_j]
represents:
- A half-closed interval
[left_j, right_j)
on the street - The average height
average_j
of all buildings in that interval
Key Points:
- If multiple buildings overlap at any position, their heights contribute to the average at that position
- The average is calculated using integer division (sum of heights divided by number of buildings)
- Consecutive segments with the same average height should be merged into one segment
- Areas with no buildings should be excluded from the output
- A half-closed interval
[a, b)
includes pointa
but excludes pointb
Example:
For buildings = [[1,5,2],[3,10,4]]
:
- From position 1 to 3: only the first building exists with height 2, so average =
2/1 = 2
- From position 3 to 5: both buildings exist with heights 2 and 4, so average =
(2+4)/2 = 3
- From position 5 to 10: only the second building exists with height 4, so average =
4/1 = 4
- Result:
[[1,3,2],[3,5,3],[5,10,4]]
Intuition
The key insight is that we need to track when buildings start and end, and how these events affect the total height and count of buildings at any given position on the street.
Think of walking along the street from left to right. As we move, buildings either start (adding to both the total height and count) or end (subtracting from both). These are the only positions where the average height can change.
This naturally leads us to think about event-based processing. We only need to care about positions where something changes - either a building starts or ends. Between these events, the average height remains constant.
To efficiently track these changes, we can use the difference array concept:
- When a building starts at position
start
, we increment the building count by 1 and add its height to the total - When a building ends at position
end
, we decrement the building count by 1 and subtract its height from the total
By processing these events in sorted order (from left to right along the street), we can maintain a running sum of:
- The current total height
s
of all active buildings - The current number
m
of active buildings
At each event position, if there are active buildings (m > 0
), we calculate the average as s // m
. This gives us a segment from the last event position to the current position with this average height.
The final optimization is merging consecutive segments with the same average height. If the segment we're about to add has the same average as the previous segment AND they're adjacent (no gap between them), we can extend the previous segment instead of creating a new one.
This approach ensures we visit each critical position exactly once and produce the minimum number of segments by merging wherever possible.
Learn more about Greedy, Sorting and Heap (Priority Queue) patterns.
Solution Approach
The implementation uses a difference array technique combined with hash tables to efficiently process building events.
Step 1: Initialize Data Structures
We use two hash tables:
cnt
: Adefaultdict(int)
to track the change in the number of buildings at each positiond
: Adefaultdict(int)
to track the change in total height at each position
Step 2: Process Building Events
For each building [start, end, height]
:
- At position
start
:cnt[start] += 1
(one more building starts)d[start] += height
(add its height to the total)
- At position
end
:cnt[end] -= 1
(one building ends)d[end] -= height
(subtract its height from the total)
Step 3: Process Events in Sorted Order
We initialize:
s = 0
: running sum of current total heightm = 0
: running count of current active buildingslast = -1
: position of the last processed eventans = []
: result array
We sort the positions in d
and process them from left to right:
for k, v in sorted(d.items()):
For each position k
:
-
Calculate segment for previous interval: If
m > 0
(there were active buildings), we have a valid segment fromlast
tok
:- Calculate average:
avg = s // m
- Check if we can merge with the previous segment:
- If
ans
is not empty AND - The last segment's average equals current
avg
AND - The last segment ends exactly at
last
(adjacent segments) - Then extend the last segment:
ans[-1][1] = k
- If
- Otherwise, add a new segment:
ans.append([last, k, avg])
- Calculate average:
-
Update running values:
s += v
: Update total height with the change at positionk
m += cnt[k]
: Update building count with the change at positionk
last = k
: Update the last processed position
Step 4: Return Result
The ans
array contains the minimum number of segments describing the street, with consecutive segments of the same average height already merged.
Time Complexity: O(n log n)
where n
is the number of buildings, due to sorting the event positions.
Space Complexity: O(n)
for storing the hash tables and result array.
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 trace through a small example: buildings = [[1,5,2],[3,10,4]]
Step 1: Build Event Maps
We create two hash tables to track changes:
- When building
[1,5,2]
starts at position 1: add 1 to count, add 2 to height - When building
[1,5,2]
ends at position 5: subtract 1 from count, subtract 2 from height - When building
[3,10,4]
starts at position 3: add 1 to count, add 4 to height - When building
[3,10,4]
ends at position 10: subtract 1 from count, subtract 4 from height
Resulting maps:
cnt = {1: 1, 5: -1, 3: 1, 10: -1}
d = {1: 2, 5: -2, 3: 4, 10: -4}
Step 2: Process Events in Sorted Order
Initialize: s = 0
(total height), m = 0
(building count), last = -1
, ans = []
Process position 1:
- Since
m = 0
(no previous buildings), skip segment creation - Update:
s = 0 + 2 = 2
,m = 0 + 1 = 1
,last = 1
Process position 3:
- Since
m = 1 > 0
, create segment[1, 3]
with average2 // 1 = 2
- Add
[1, 3, 2]
toans
- Update:
s = 2 + 4 = 6
,m = 1 + 1 = 2
,last = 3
Process position 5:
- Since
m = 2 > 0
, create segment[3, 5]
with average6 // 2 = 3
- Check merge: last segment is
[1, 3, 2]
with average 2 ≠3, so no merge - Add
[3, 5, 3]
toans
- Update:
s = 6 - 2 = 4
,m = 2 - 1 = 1
,last = 5
Process position 10:
- Since
m = 1 > 0
, create segment[5, 10]
with average4 // 1 = 4
- Check merge: last segment is
[3, 5, 3]
with average 3 ≠4, so no merge - Add
[5, 10, 4]
toans
- Update:
s = 4 - 4 = 0
,m = 1 - 1 = 0
,last = 10
Final Result: [[1,3,2],[3,5,3],[5,10,4]]
This correctly represents:
- From 1 to 3: only building 1 (height 2)
- From 3 to 5: both buildings (heights 2 and 4, average 3)
- From 5 to 10: only building 2 (height 4)
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def averageHeightOfBuildings(self, buildings: List[List[int]]) -> List[List[int]]:
6 # Track the number of buildings starting/ending at each position
7 building_count_changes = defaultdict(int)
8 # Track the total height changes at each position
9 height_changes = defaultdict(int)
10
11 # Process each building's contribution to the height and count changes
12 for start_pos, end_pos, height in buildings:
13 # Increment count at start, decrement at end
14 building_count_changes[start_pos] += 1
15 building_count_changes[end_pos] -= 1
16 # Add height at start, subtract at end
17 height_changes[start_pos] += height
18 height_changes[end_pos] -= height
19
20 # Initialize tracking variables
21 total_height = 0 # Running sum of heights
22 building_count = 0 # Current number of overlapping buildings
23 last_position = -1 # Previous position processed
24 result = []
25
26 # Process positions in sorted order
27 for current_position, height_change in sorted(height_changes.items()):
28 # If there were buildings in the previous segment
29 if building_count > 0:
30 average_height = total_height // building_count
31
32 # Check if we can merge with the previous segment
33 if (result and
34 result[-1][2] == average_height and
35 result[-1][1] == last_position):
36 # Extend the previous segment
37 result[-1][1] = current_position
38 else:
39 # Add new segment
40 result.append([last_position, current_position, average_height])
41
42 # Update running totals for the next segment
43 total_height += height_change
44 building_count += building_count_changes[current_position]
45 last_position = current_position
46
47 return result
48
1class Solution {
2 public int[][] averageHeightOfBuildings(int[][] buildings) {
3 // Map to track the count of buildings at each position
4 // Positive value means building starts, negative means building ends
5 Map<Integer, Integer> buildingCount = new HashMap<>();
6
7 // TreeMap to track the cumulative height changes at each position
8 // Sorted by position to process events in order
9 TreeMap<Integer, Integer> heightChanges = new TreeMap<>();
10
11 // Process each building to record start/end events
12 for (int[] building : buildings) {
13 int startPos = building[0];
14 int endPos = building[1];
15 int height = building[2];
16
17 // Increment count at start position, decrement at end position
18 buildingCount.merge(startPos, 1, Integer::sum);
19 buildingCount.merge(endPos, -1, Integer::sum);
20
21 // Add height at start position, subtract at end position
22 heightChanges.merge(startPos, height, Integer::sum);
23 heightChanges.merge(endPos, -height, Integer::sum);
24 }
25
26 // Variables to track current state while sweeping through positions
27 int currentTotalHeight = 0; // Sum of heights of active buildings
28 int currentBuildingCount = 0; // Number of active buildings
29 int previousPosition = -1; // Previous position processed
30
31 // List to store the result intervals
32 List<int[]> result = new ArrayList<>();
33
34 // Sweep through all positions in sorted order
35 for (Map.Entry<Integer, Integer> entry : heightChanges.entrySet()) {
36 int currentPosition = entry.getKey();
37 int heightChange = entry.getValue();
38
39 // If there were active buildings in the previous interval
40 if (currentBuildingCount > 0) {
41 int averageHeight = currentTotalHeight / currentBuildingCount;
42
43 // Check if we can merge with the last interval
44 // Merge if the average height is the same and intervals are consecutive
45 if (!result.isEmpty() &&
46 result.get(result.size() - 1)[2] == averageHeight &&
47 result.get(result.size() - 1)[1] == previousPosition) {
48 // Extend the last interval's end position
49 result.get(result.size() - 1)[1] = currentPosition;
50 } else {
51 // Add a new interval
52 result.add(new int[] {previousPosition, currentPosition, averageHeight});
53 }
54 }
55
56 // Update current state for the next iteration
57 currentTotalHeight += heightChange;
58 currentBuildingCount += buildingCount.get(currentPosition);
59 previousPosition = currentPosition;
60 }
61
62 // Convert list to 2D array and return
63 return result.toArray(new int[0][]);
64 }
65}
66
1class Solution {
2public:
3 vector<vector<int>> averageHeightOfBuildings(vector<vector<int>>& buildings) {
4 // Track the change in number of buildings at each position
5 unordered_map<int, int> buildingCountDelta;
6 // Track the change in total height at each position (sorted by position)
7 map<int, int> heightDelta;
8
9 // Process each building to create event points at start and end positions
10 for (const auto& building : buildings) {
11 int startPos = building[0];
12 int endPos = building[1];
13 int height = building[2];
14
15 // At start position: increment building count, add height
16 buildingCountDelta[startPos]++;
17 buildingCountDelta[endPos]--;
18 heightDelta[startPos] += height;
19 heightDelta[endPos] -= height;
20 }
21
22 // Variables to track current state while sweeping through positions
23 int currentTotalHeight = 0; // Sum of heights of overlapping buildings
24 int currentBuildingCount = 0; // Number of overlapping buildings
25 int previousPosition = -1; // Previous position in the sweep
26 vector<vector<int>> result;
27
28 // Sweep through all positions in sorted order
29 for (const auto& [position, heightChange] : heightDelta) {
30 // If there were buildings in the previous interval, calculate average
31 if (currentBuildingCount > 0) {
32 int averageHeight = currentTotalHeight / currentBuildingCount;
33
34 // Merge with previous interval if it has the same average height
35 if (!result.empty() &&
36 result.back()[2] == averageHeight &&
37 result.back()[1] == previousPosition) {
38 // Extend the previous interval
39 result.back()[1] = position;
40 } else {
41 // Add new interval with average height
42 result.push_back({previousPosition, position, averageHeight});
43 }
44 }
45
46 // Update current state for next interval
47 currentTotalHeight += heightChange;
48 currentBuildingCount += buildingCountDelta[position];
49 previousPosition = position;
50 }
51
52 return result;
53 }
54};
55
1/**
2 * Calculates the average height of overlapping buildings at each position
3 * @param buildings - Array of buildings where each element is [start, end, height]
4 * @returns Array of intervals with average heights [start, end, averageHeight]
5 */
6function averageHeightOfBuildings(buildings: number[][]): number[][] {
7 // Map to track the count of buildings at each position (positive at start, negative at end)
8 const buildingCountChanges = new Map<number, number>();
9 // Map to track the height changes at each position
10 const heightChanges = new Map<number, number>();
11
12 // Process each building to record changes at start and end positions
13 for (const [start, end, height] of buildings) {
14 // Increment building count at start position
15 buildingCountChanges.set(start, (buildingCountChanges.get(start) || 0) + 1);
16 // Decrement building count at end position
17 buildingCountChanges.set(end, (buildingCountChanges.get(end) || 0) - 1);
18 // Add height at start position
19 heightChanges.set(start, (heightChanges.get(start) || 0) + height);
20 // Subtract height at end position
21 heightChanges.set(end, (heightChanges.get(end) || 0) - height);
22 }
23
24 // Initialize tracking variables
25 let totalHeight = 0; // Running sum of heights
26 let buildingCount = 0; // Current number of overlapping buildings
27 let lastPosition = -1; // Previous position processed
28 const result: number[][] = [];
29
30 // Get all positions and sort them in ascending order
31 const sortedPositions = Array.from(heightChanges.keys()).sort((a, b) => a - b);
32
33 // Process each position in order
34 for (const position of sortedPositions) {
35 const heightChange = heightChanges.get(position)!;
36
37 // If there were buildings in the previous interval, calculate average
38 if (buildingCount > 0) {
39 const averageHeight = Math.floor(totalHeight / buildingCount);
40
41 // Check if we can merge with the previous interval (same average height and contiguous)
42 if (result.length > 0 &&
43 result.at(-1)![2] === averageHeight &&
44 result.at(-1)![1] === lastPosition) {
45 // Extend the previous interval's end position
46 result[result.length - 1][1] = position;
47 } else {
48 // Add new interval with calculated average
49 result.push([lastPosition, position, averageHeight]);
50 }
51 }
52
53 // Update running totals for next iteration
54 totalHeight += heightChange;
55 buildingCount += buildingCountChanges.get(position)!;
56 lastPosition = position;
57 }
58
59 return result;
60}
61
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by the sorting operation sorted(d.items())
. The dictionary d
contains at most 2n
entries (where n
is the number of buildings), since each building contributes at most 2 unique positions (start and end). Sorting these entries takes O(2n × log(2n))
which simplifies to O(n × log n)
. The initial loop to populate the dictionaries takes O(n)
time, and the final loop to build the answer takes O(n)
time. Therefore, the overall time complexity is O(n × log n)
.
Space Complexity: O(n)
The space complexity comes from the auxiliary data structures used:
- Dictionary
cnt
: stores at most2n
entries →O(n)
- Dictionary
d
: stores at most2n
entries →O(n)
- List
ans
: in the worst case, storesO(n)
intervals →O(n)
- The sorted operation creates a temporary list of size
O(n)
The total space complexity is therefore O(n)
, where n
is the number of buildings.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrectly Handling Overlapping Buildings
Pitfall: A common mistake is trying to process buildings individually or attempting to merge overlapping intervals first, which can lead to incorrect average calculations. For instance, if you process buildings sequentially and try to maintain segments directly, you might incorrectly calculate averages when buildings partially overlap.
Example of incorrect approach:
# WRONG: Processing buildings one by one for start, end, height in buildings: # Trying to directly insert segments result.append([start, end, height]) # Then trying to merge overlapping segments
Solution: Use the event-based approach with difference arrays. By tracking changes at specific positions rather than intervals, you ensure that all overlapping buildings are correctly accounted for at each position.
2. Forgetting to Merge Adjacent Segments with Same Average
Pitfall: The problem requires the minimum number of segments. A common error is forgetting to merge consecutive segments that have the same average height, resulting in unnecessary fragmentation.
Example of the issue:
# If buildings = [[1,3,2],[3,5,2]] # Without merging: [[1,3,2],[3,5,2]] # WRONG - 2 segments # With merging: [[1,5,2]] # CORRECT - 1 segment
Solution: Always check if the current segment can be merged with the previous one:
if (result and result[-1][2] == average_height and result[-1][1] == last_position): # Adjacent check result[-1][1] = current_position # Extend previous segment
3. Using Floating-Point Division Instead of Integer Division
Pitfall: The problem specifically states that averages should be calculated using integer division. Using floating-point division (/
) instead of integer division (//
) will produce incorrect results.
Wrong:
average_height = total_height / building_count # Returns float
Correct:
average_height = total_height // building_count # Returns integer
4. Not Properly Tracking Building Count Changes
Pitfall: Only tracking height changes without properly tracking the number of buildings at each position. This leads to division errors or incorrect averages.
Example of incomplete tracking:
# WRONG: Only tracking heights for start, end, height in buildings: height_changes[start] += height height_changes[end] -= height # Missing: building_count_changes tracking!
Solution: Maintain separate tracking for both building counts and heights:
building_count_changes[start] += 1 building_count_changes[end] -= 1 height_changes[start] += height height_changes[end] -= height
5. Processing Events in Wrong Order
Pitfall: Not sorting the positions before processing them. The algorithm requires processing events from left to right to correctly maintain running totals.
Wrong:
# Processing in arbitrary order for k, v in height_changes.items(): # Not sorted!
Correct:
for k, v in sorted(height_changes.items()): # Sorted by position
6. Including Empty Intervals in the Result
Pitfall: Adding segments where no buildings exist (where building_count = 0
). The problem states that areas with no buildings should be excluded.
Solution: Only create segments when there are active buildings:
if building_count > 0: # Only process if buildings exist average_height = total_height // building_count # Add segment to result
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
https assets algo monster cover_photos heap svg Priority Queue and Heap What is the relationship between priority queue and heap Priority Queue is an Abstract Data Type and Heap is the concrete data structure we use to implement a priority queue Priority Queue A priority queue is a data structure
Want a Structured Path to Master System Design Too? Don’t Miss This!