2345. Finding the Number of Visible Mountains 🔒
Problem Description
You are given a list of mountain peaks, where each peak is represented by its coordinates [x, y]
. Each mountain forms a right-angled isosceles triangle shape with:
- Its base along the x-axis
- A right angle at the peak point
(x, y)
- Slopes of
1
and-1
for the ascending and descending sides
A mountain is considered visible if its peak is not located inside or on the border of another mountain.
Your task is to count how many mountains are visible.
For example, if a smaller mountain is completely covered by a larger mountain, the smaller one is not visible. The key insight is that each mountain (x, y)
can be represented as an interval on the x-axis from (x - y)
to (x + y)
, where y
is the height. A mountain is not visible if its interval is completely contained within another mountain's interval.
The solution converts each mountain peak (x, y)
into an interval (x - y, x + y)
representing its left and right boundaries on the x-axis. Then it sorts these intervals and checks which mountains remain visible by tracking if their intervals are covered by others. Mountains with duplicate intervals are handled specially - if multiple mountains have the exact same interval, they all overlap perfectly and only need to be counted based on visibility rules.
Intuition
The key observation is that each mountain forms a right-angled isosceles triangle. Since the slopes are 1
and -1
, if a mountain has its peak at (x, y)
, the left base point is at (x - y, 0)
and the right base point is at (x + y, 0)
. This means we can represent each mountain as an interval [x - y, x + y]
on the x-axis.
Once we have this interval representation, the problem becomes: given a set of intervals, find how many intervals are not completely contained within another interval. This is a classic interval problem.
To determine which intervals are "dominated" by others, we need a clever sorting strategy. We sort by:
- Left endpoint in ascending order (smaller left points first)
- When left endpoints are equal, right endpoint in descending order (larger right points first)
This sorting ensures that if one interval can contain another, the containing interval comes first. As we traverse the sorted intervals, we maintain the maximum right endpoint seen so far. If an interval's right endpoint doesn't extend beyond this maximum, it means this interval is completely contained within a previous one and thus not visible.
There's one special case to handle: when multiple mountains have identical intervals (same [x - y, x + y]
), they perfectly overlap. We use a counter to track duplicates - only intervals that appear exactly once can contribute to the visible count, as duplicate intervals represent mountains that completely overlap each other at the same position.
The algorithm efficiently identifies visible mountains by converting the geometric problem into an interval coverage problem, which can be solved in O(n log n)
time through sorting and a single pass.
Learn more about Stack, Sorting and Monotonic Stack patterns.
Solution Approach
The implementation follows these steps:
-
Convert mountains to intervals: For each mountain peak
(x, y)
, calculate its interval representation as(x - y, x + y)
. This gives us the left and right boundaries on the x-axis.arr = [(x - y, x + y) for x, y in peaks]
-
Count duplicates: Use a Counter to track how many times each interval appears. This helps identify mountains that perfectly overlap.
cnt = Counter(arr)
-
Sort intervals strategically: Sort the intervals by left endpoint ascending, and when left endpoints are equal, by right endpoint descending. This ensures larger intervals come before smaller ones when they share the same left endpoint.
arr.sort(key=lambda x: (x[0], -x[1]))
-
Traverse and track visibility: Initialize variables:
ans
to count visible mountainscur
to track the maximum right endpoint seen so far (initialized to negative infinity)
For each interval
(l, r)
:- If
r <= cur
, this interval is completely contained within a previous interval, so skip it - Otherwise, update
cur = r
as we've found an interval that extends further right - If this interval appears only once (
cnt[(l, r)] == 1
), increment the answer
ans, cur = 0, -inf for l, r in arr: if r <= cur: continue cur = r if cnt[(l, r)] == 1: ans += 1
The algorithm works because:
- The sorting ensures we process potentially containing intervals before contained ones
- By tracking the maximum right endpoint, we can quickly determine if an interval is covered
- The duplicate check ensures mountains with identical intervals don't all get counted as visible
Time complexity: O(n log n)
for sorting, where n
is the number of mountains.
Space complexity: O(n)
for storing the intervals and counter.
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 walk through a small example with peaks: [[2,2], [6,3], [5,4]]
Step 1: Convert mountains to intervals
- Peak
(2,2)
: interval =(2-2, 2+2)
=(0, 4)
- Peak
(6,3)
: interval =(6-3, 6+3)
=(3, 9)
- Peak
(5,4)
: interval =(5-4, 5+4)
=(1, 9)
So arr = [(0,4), (3,9), (1,9)]
Step 2: Count duplicates
cnt = {(0,4): 1, (3,9): 1, (1,9): 1}
- each interval appears once
Step 3: Sort intervals Sort by left endpoint ascending, then right endpoint descending:
(0,4)
- left=0(1,9)
- left=1(3,9)
- left=3
Sorted: arr = [(0,4), (1,9), (3,9)]
Step 4: Traverse and track visibility
- Initialize:
ans = 0
,cur = -inf
Process (0,4)
:
4 > -inf
, so not contained- Update
cur = 4
cnt[(0,4)] = 1
, soans = 1
Process (1,9)
:
9 > 4
, so not contained- Update
cur = 9
cnt[(1,9)] = 1
, soans = 2
Process (3,9)
:
9 <= 9
, so this interval is contained (doesn't extend beyond current max)- Skip it, don't increment
ans
Result: 2 visible mountains
The mountains at peaks (2,2)
and (5,4)
are visible. The mountain at (6,3)
is completely contained within the mountain at (5,4)
since its interval (3,9)
doesn't extend beyond the interval (1,9)
.
Solution Implementation
1from typing import List
2from collections import Counter
3
4class Solution:
5 def visibleMountains(self, peaks: List[List[int]]) -> int:
6 # Convert each peak [x, y] to interval representation [left_base, right_base]
7 # A mountain with peak at (x, y) forms a triangle with base from (x-y) to (x+y)
8 intervals = [(x - y, x + y) for x, y in peaks]
9
10 # Count occurrences of each interval (to handle duplicate mountains)
11 interval_counts = Counter(intervals)
12
13 # Sort intervals by left endpoint ascending, then by right endpoint descending
14 # This ensures larger mountains come before smaller ones with same left endpoint
15 intervals.sort(key=lambda interval: (interval[0], -interval[1]))
16
17 visible_count = 0
18 max_right_seen = float('-inf') # Track the rightmost point covered so far
19
20 # Iterate through sorted intervals to determine visibility
21 for left, right in intervals:
22 # If current mountain is completely hidden by previous mountains, skip it
23 if right <= max_right_seen:
24 continue
25
26 # Update the rightmost point covered
27 max_right_seen = right
28
29 # Only count this mountain if it's unique (not a duplicate)
30 if interval_counts[(left, right)] == 1:
31 visible_count += 1
32
33 return visible_count
34
1class Solution {
2 public int visibleMountains(int[][] peaks) {
3 int numberOfPeaks = peaks.length;
4
5 // Transform each peak into interval representation
6 // For a peak at (x, y), the mountain base spans from (x-y) to (x+y)
7 int[][] intervals = new int[numberOfPeaks][2];
8 for (int i = 0; i < numberOfPeaks; ++i) {
9 int xCoordinate = peaks[i][0];
10 int yCoordinate = peaks[i][1];
11 intervals[i] = new int[] {xCoordinate - yCoordinate, xCoordinate + yCoordinate};
12 }
13
14 // Sort intervals by left endpoint ascending, then by right endpoint descending
15 // This ensures larger mountains come first when they share the same left endpoint
16 Arrays.sort(intervals, (a, b) -> {
17 if (a[0] == b[0]) {
18 return b[1] - a[1]; // Sort by right endpoint descending
19 }
20 return a[0] - b[0]; // Sort by left endpoint ascending
21 });
22
23 int visibleCount = 0;
24 int currentMaxRight = Integer.MIN_VALUE;
25
26 // Iterate through sorted intervals to count visible mountains
27 for (int i = 0; i < numberOfPeaks; ++i) {
28 int leftEndpoint = intervals[i][0];
29 int rightEndpoint = intervals[i][1];
30
31 // Skip if current mountain is completely covered by previous mountains
32 if (rightEndpoint <= currentMaxRight) {
33 continue;
34 }
35
36 // Update the rightmost point covered so far
37 currentMaxRight = rightEndpoint;
38
39 // Check if current mountain is not a duplicate
40 // A mountain is counted as visible only if it's unique at this position
41 boolean isNotDuplicate = !(i < numberOfPeaks - 1 &&
42 intervals[i][0] == intervals[i + 1][0] &&
43 intervals[i][1] == intervals[i + 1][1]);
44
45 if (isNotDuplicate) {
46 ++visibleCount;
47 }
48 }
49
50 return visibleCount;
51 }
52}
53
1class Solution {
2public:
3 int visibleMountains(vector<vector<int>>& peaks) {
4 // Transform each mountain peak (x, y) into an interval representation
5 // A mountain with peak at (x, y) forms a triangle with:
6 // - Left boundary: x - y
7 // - Right boundary: x + y
8 // Store as (left, -right) for sorting purposes
9 vector<pair<int, int>> intervals;
10 for (auto& peak : peaks) {
11 int x = peak[0], y = peak[1];
12 int leftBoundary = x - y;
13 int rightBoundary = x + y;
14 // Store negative right boundary to handle sorting correctly
15 // Mountains with same left boundary will be sorted by descending right boundary
16 intervals.emplace_back(leftBoundary, -rightBoundary);
17 }
18
19 // Sort intervals by left boundary first, then by right boundary (descending)
20 sort(intervals.begin(), intervals.end());
21
22 int n = intervals.size();
23 int visibleCount = 0;
24 int maxRightBoundary = INT_MIN; // Track the rightmost boundary seen so far
25
26 for (int i = 0; i < n; ++i) {
27 int currentLeft = intervals[i].first;
28 int currentRight = -intervals[i].second; // Convert back to positive
29
30 // If current mountain is completely hidden by previous mountains, skip it
31 if (currentRight <= maxRightBoundary) {
32 continue;
33 }
34
35 // Update the maximum right boundary
36 maxRightBoundary = currentRight;
37
38 // Count this mountain as visible only if:
39 // 1. It's the last mountain, OR
40 // 2. It's not identical to the next mountain (handling duplicates)
41 bool isLastMountain = (i == n - 1);
42 bool isDifferentFromNext = (i < n - 1 && intervals[i] != intervals[i + 1]);
43
44 if (isLastMountain || isDifferentFromNext) {
45 visibleCount++;
46 }
47 }
48
49 return visibleCount;
50 }
51};
52
1function visibleMountains(peaks: number[][]): number {
2 // Transform each mountain peak (x, y) into an interval representation
3 // A mountain with peak at (x, y) forms a triangle with:
4 // - Left boundary: x - y
5 // - Right boundary: x + y
6 // Store as [left, -right] for sorting purposes
7 const intervals: [number, number][] = [];
8
9 for (const peak of peaks) {
10 const x = peak[0];
11 const y = peak[1];
12 const leftBoundary = x - y;
13 const rightBoundary = x + y;
14 // Store negative right boundary to handle sorting correctly
15 // Mountains with same left boundary will be sorted by descending right boundary
16 intervals.push([leftBoundary, -rightBoundary]);
17 }
18
19 // Sort intervals by left boundary first, then by right boundary (descending)
20 intervals.sort((a, b) => {
21 if (a[0] !== b[0]) {
22 return a[0] - b[0];
23 }
24 return a[1] - b[1];
25 });
26
27 const n = intervals.length;
28 let visibleCount = 0;
29 let maxRightBoundary = Number.MIN_SAFE_INTEGER; // Track the rightmost boundary seen so far
30
31 for (let i = 0; i < n; i++) {
32 const currentLeft = intervals[i][0];
33 const currentRight = -intervals[i][1]; // Convert back to positive
34
35 // If current mountain is completely hidden by previous mountains, skip it
36 if (currentRight <= maxRightBoundary) {
37 continue;
38 }
39
40 // Update the maximum right boundary
41 maxRightBoundary = currentRight;
42
43 // Count this mountain as visible only if:
44 // 1. It's the last mountain, OR
45 // 2. It's not identical to the next mountain (handling duplicates)
46 const isLastMountain = (i === n - 1);
47 const isDifferentFromNext = (i < n - 1 &&
48 (intervals[i][0] !== intervals[i + 1][0] ||
49 intervals[i][1] !== intervals[i + 1][1]));
50
51 if (isLastMountain || isDifferentFromNext) {
52 visibleCount++;
53 }
54 }
55
56 return visibleCount;
57}
58
Time and Space Complexity
Time Complexity: O(n × log n)
The time complexity is dominated by the sorting operation. Breaking down the algorithm:
- Converting peaks to intervals
(x - y, x + y)
takesO(n)
time - Creating a Counter from the intervals takes
O(n)
time - Sorting the array of intervals takes
O(n × log n)
time - The final loop iterates through all intervals once, taking
O(n)
time
Since sorting is the most expensive operation, the overall time complexity is O(n × log n)
, where n
is the number of mountains.
Space Complexity: O(n)
The space complexity analysis:
- The
arr
list storesn
transformed intervals, requiringO(n)
space - The
Counter
objectcnt
stores at mostn
unique interval entries, requiringO(n)
space - The sorting operation may use
O(log n)
toO(n)
additional space depending on the implementation - Other variables (
ans
,cur
) useO(1)
space
Therefore, the total space complexity is O(n)
, where n
is the number of mountains.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Forgetting to Handle Duplicate Mountains
One of the most common mistakes is not properly handling mountains that have identical intervals. When multiple mountains have the exact same base interval (x-y, x+y)
, they completely overlap each other, making all of them "invisible" according to the problem's visibility rules.
Incorrect approach:
def visibleMountains(self, peaks: List[List[int]]) -> int:
intervals = [(x - y, x + y) for x, y in peaks]
intervals.sort(key=lambda interval: (interval[0], -interval[1]))
visible_count = 0
max_right_seen = float('-inf')
for left, right in intervals:
if right <= max_right_seen:
continue
max_right_seen = right
visible_count += 1 # Wrong! Counts duplicates as visible
return visible_count
Why it fails: If peaks [2, 2]
and [2, 2]
both exist, they create identical intervals (0, 4)
. The incorrect code would count one as visible, but since they perfectly overlap, neither should be counted.
Solution: Use a Counter to track duplicate intervals and only count intervals that appear exactly once:
interval_counts = Counter(intervals) # ... in the loop: if interval_counts[(left, right)] == 1: visible_count += 1
2. Incorrect Sorting Strategy
Another pitfall is using the wrong sorting key, which can lead to processing smaller mountains before larger ones that share the same left endpoint.
Incorrect approach:
intervals.sort() # Default sort: by left, then by right ascending
Why it fails: With default sorting, an interval like (0, 3)
would come before (0, 5)
. When we process (0, 3)
first, we'd mark it as visible and update max_right_seen = 3
. Then when processing (0, 5)
, we'd also mark it as visible. However, (0, 3)
should actually be hidden by (0, 5)
.
Solution: Sort by left endpoint ascending, but right endpoint descending when left endpoints are equal:
intervals.sort(key=lambda interval: (interval[0], -interval[1]))
This ensures that when two intervals share the same left endpoint, the one with the larger right endpoint (the bigger mountain) is processed first.
3. Not Removing Duplicates from the Sorted List
Some might try to remove duplicates after sorting but before the visibility check, which can lead to inefficient code or logical errors.
Problematic approach:
intervals = list(set(intervals)) # Removes duplicates but loses count information
intervals.sort(key=lambda interval: (interval[0], -interval[1]))
Why it's problematic: While this removes duplicates, you lose the information about which mountains were duplicates, making it impossible to correctly determine that none of the duplicate mountains should be counted as visible.
Better solution: Keep all intervals in the sorted list but use the Counter to track which ones are duplicates, allowing you to process each interval exactly once while knowing whether it had duplicates.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!