LeetCode 1944 - Number of Visible People in a Queue
The problem gives a queue of n people standing from left to right, where each person has a distinct height. For each person i, we need to determine how many people to their right they can see.
Difficulty: 🔴 Hard
Topics: Array, Stack, Monotonic Stack
Solution
Problem Understanding
The problem gives a queue of n people standing from left to right, where each person has a distinct height. For each person i, we need to determine how many people to their right they can see.
A person i can see a person j (with j > i) if every person between them is strictly shorter than both endpoints, meaning the tallest person in the interval (i+1 ... j-1) is smaller than min(heights[i], heights[j]). In simpler terms, visibility is blocked only when there is an intermediate person taller than or equal to the limiting height between the two endpoints.
The output is an array answer where answer[i] represents how many people to the right of index i are visible under this rule.
The constraints are significant: n can be up to 10^5, so an O(n^2) solution is too slow. Heights are distinct, which simplifies comparisons because we do not need to handle ties.
Important edge cases include a strictly decreasing sequence, where each person can see many others, and a strictly increasing sequence, where each person sees only the first taller person. Another key edge case is the last element, which always sees zero people because no one is to the right.
Approaches
Brute Force Approach
The brute-force method iterates from each index i to the right, scanning every j > i and checking whether j is visible. To determine visibility, we maintain the maximum height seen between i and j, and ensure it never exceeds min(heights[i], heights[j]). If visibility holds, we increment the count for i.
This approach is correct because it directly simulates the definition of visibility. However, for each i, we may scan up to n elements, and maintaining intermediate maxima leads to repeated work, making it too slow for large inputs.
Optimal Approach: Monotonic Stack from Right to Left
The key insight is that for each person i, only certain people to the right can contribute to visibility:
- The first person taller than
heights[i]is always visible (because no taller blocker exists between them). - After that, shorter people may also be visible, but they are "consumed" in a chain-like manner.
- A monotonic decreasing stack (from right to left perspective) helps efficiently track candidates that are visible or block visibility.
We traverse from right to left, maintaining a stack of indices with decreasing heights. For each person i, we repeatedly pop shorter people from the stack, counting them as visible. If the stack is not empty afterward, the top element is also visible (the first taller blocker), so we count it as well.
This works because once a shorter person is popped, they cannot block anyone further left, and taller people naturally dominate visibility boundaries.
Comparison Table
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n^2) | O(1) | Checks visibility for every pair directly |
| Optimal Monotonic Stack | O(n) | O(n) | Each element pushed and popped at most once |
Algorithm Walkthrough
- Initialize an empty stack that will store indices of people to the right. The stack will maintain a decreasing height structure, meaning heights from top to bottom are strictly decreasing. This structure ensures we always know the nearest taller candidates efficiently.
- Create an answer array initialized with zeros. This will store visibility counts for each person.
- Iterate from right to left over the array. We go right-to-left because we want to progressively build knowledge of what each person can see among already-processed people.
- For each person
i, initialize a local countercount = 0. This counter will track how many peopleican see. - While the stack is not empty and the current person is taller than the person at the top of the stack, pop the stack and increment
count. Each popped person is visible because no taller blocker exists betweeniand that person. - If the stack is still not empty after popping, it means there is a taller person blocking further view but still visible to
i. Incrementcountonce more to account for this first taller person. - Push the current index
ionto the stack so it becomes a potential visible candidate for earlier elements. - Store
countinanswer[i].
Why it works
The algorithm relies on the invariant that the stack always maintains a decreasing sequence of heights from top to bottom. This ensures that when processing a new element, all shorter elements are immediately visible and removable, and the first remaining taller element is the next blocking boundary. Each element is pushed and popped at most once, preserving correctness and efficiency.
Python Solution
from typing import List
class Solution:
def canSeePersonsCount(self, heights: List[int]) -> List[int]:
n = len(heights)
stack = []
answer = [0] * n
for i in range(n - 1, -1, -1):
count = 0
while stack and heights[i] > heights[stack[-1]]:
stack.pop()
count += 1
if stack:
count += 1
stack.append(i)
answer[i] = count
return answer
Code Explanation
The solution iterates from right to left, ensuring that when processing index i, all relevant candidates to the right are already in the stack. The stack stores indices in decreasing height order. The while loop removes all shorter people, each of whom is visible to i. After that, if any taller person remains, they are counted once because they are the first blocking but visible person. Finally, i is pushed for future iterations, and its computed visibility count is stored.
Go Solution
func canSeePersonsCount(heights []int) []int {
n := len(heights)
stack := make([]int, 0, n)
answer := make([]int, n)
for i := n - 1; i >= 0; i-- {
count := 0
for len(stack) > 0 && heights[i] > heights[stack[len(stack)-1]] {
stack = stack[:len(stack)-1]
count++
}
if len(stack) > 0 {
count++
}
stack = append(stack, i)
answer[i] = count
}
return answer
}
Go-Specific Notes
The Go implementation mirrors the Python logic closely. The main difference is explicit slice manipulation to simulate stack behavior. The stack is a slice of indices, and popping is done by reslicing. Since Go integers are safe for this range, no overflow concerns exist. The logic remains identical in structure and guarantees O(n) performance.
Worked Examples
Example 1: [10,6,8,5,11,9]
We process from right to left.
| i | height | stack (indices) | pops | visible count | answer |
|---|---|---|---|---|---|
| 5 | 9 | [] | none | 0 | [,,,,_,0] |
| 4 | 11 | [5] | none | 1 (sees 9) | [,,,,1,0] |
| 3 | 5 | [4] | none | 1 (sees 11) | [,,_,1,1,0] |
| 2 | 8 | [4,3] → pop 3 | 1 pop + 1 taller | 2 | [,,2,1,1,0] |
| 1 | 6 | [4,2] → pop 2,3? (via logic) | 1 + 1 | 1 | [_,1,2,1,1,0] |
| 0 | 10 | [4,2,1] → pop 1,2 | 2 pops + 1 taller | 3 | [3,1,2,1,1,0] |
This shows how smaller elements are progressively removed and taller boundaries contribute exactly one visibility each time.
Example 2: [5,1,2,3,10]
| i | height | stack | pops | visible | answer |
|---|---|---|---|---|---|
| 4 | 10 | [] | 0 | 0 | [,,,,0] |
| 3 | 3 | [4] | 0 | 1 | [,,_,1,0] |
| 2 | 2 | [4,3] | 1 | 1 | [,,1,1,0] |
| 1 | 1 | [4,3,2] | 2 | 1 | [_,1,1,1,0] |
| 0 | 5 | [4] | 3 pops (1,2,3) | 4 | [4,1,1,1,0] |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Each index is pushed once and popped at most once |
| Space | O(n) | Stack stores indices in worst case |
The linear time complexity comes from the amortized behavior of the monotonic stack. Even though there is a nested loop, each element enters and leaves the stack only once, ensuring total operations remain linear.
Test Cases
assert Solution().canSeePersonsCount([10,6,8,5,11,9]) == [3,1,2,1,1,0] # example 1
assert Solution().canSeePersonsCount([5,1,2,3,10]) == [4,1,1,1,0] # example 2
assert Solution().canSeePersonsCount([1]) == [0] # single element
assert Solution().canSeePersonsCount([5,4,3,2,1]) == [4,3,2,1,0] # strictly decreasing
assert Solution().canSeePersonsCount([1,2,3,4,5]) == [1,1,1,1,0] # strictly increasing
assert Solution().canSeePersonsCount([3,1,4,2,5]) == [2,1,1,1,0] # mixed pattern
| Test | Why |
|---|---|
| single element | verifies base case handling |
| decreasing sequence | tests maximum visibility chaining |
| increasing sequence | ensures only first taller is counted |
| mixed pattern | validates stack popping logic |
Edge Cases
One important edge case is a single-element input. Since no elements exist to the right, the output must always be [0]. The algorithm handles this naturally because the loop processes the last element with an empty stack.
Another edge case is strictly decreasing heights. In this scenario, each person can see all people to their right. The stack will continuously pop every element, and no blocking taller element will exist, resulting in maximal visibility counts.
A strictly increasing sequence is also critical. Each person can only see the next person because all subsequent people are blocked by the first taller one. The stack never pops many elements in this case, ensuring only one visible person per index except the last.
Finally, large inputs up to 10^5 require the algorithm to avoid quadratic behavior. The monotonic stack guarantees each element is processed once, preventing timeouts and ensuring scalability.