Facebook Pixel

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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Left endpoint in ascending order (smaller left points first)
  2. 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:

  1. 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]
  2. Count duplicates: Use a Counter to track how many times each interval appears. This helps identify mountains that perfectly overlap.

    cnt = Counter(arr)
  3. 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]))
  4. Traverse and track visibility: Initialize variables:

    • ans to count visible mountains
    • cur 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 Evaluator

Example 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, so ans = 1

Process (1,9):

  • 9 > 4, so not contained
  • Update cur = 9
  • cnt[(1,9)] = 1, so ans = 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) takes O(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 stores n transformed intervals, requiring O(n) space
  • The Counter object cnt stores at most n unique interval entries, requiring O(n) space
  • The sorting operation may use O(log n) to O(n) additional space depending on the implementation
  • Other variables (ans, cur) use O(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.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More