962. Maximum Width Ramp
Problem Description
You are given an integer array nums
. A ramp is defined as a pair of indices (i, j)
where:
i < j
(i comes before j in the array)nums[i] <= nums[j]
(the value at index i is less than or equal to the value at index j)
The width of a ramp is calculated as j - i
(the distance between the two indices).
Your task is to find the maximum width among all possible ramps in the array. If no valid ramp exists in nums
, return 0
.
For example:
- If
nums = [6, 0, 8, 2, 1, 5]
, a valid ramp would be(i=1, j=5)
becausenums[1] = 0 <= nums[5] = 5
, giving a width of5 - 1 = 4
. - Another valid ramp would be
(i=0, j=2)
becausenums[0] = 6 <= nums[2] = 8
, giving a width of2 - 0 = 2
.
The solution uses a monotonic stack approach:
- First, build a decreasing monotonic stack by iterating through the array and only adding indices where the value is smaller than the previous stack top. This creates a list of potential starting points for ramps.
- Then, iterate from right to left through the array. For each position, check if it can form a valid ramp with indices in the stack (where the stack value is less than or equal to current value). Pop elements from the stack when valid ramps are found and track the maximum width.
- The key insight is that once an index from the stack is used as the start of a ramp, it can be removed since any future ramp using that starting point would have a smaller width.
Intuition
To find the maximum width ramp, we need to find indices i
and j
where i < j
and nums[i] <= nums[j]
, maximizing j - i
.
The key observation is that for any starting position i
of a ramp, we want to find the rightmost j
where nums[j] >= nums[i]
. This naturally suggests we should process the array from right to left when looking for ending positions.
But which indices should we consider as potential starting positions? If we have two indices i1 < i2
where nums[i1] <= nums[i2]
, then i1
is always a better choice as a starting position because:
- Any
j
that forms a valid ramp withi2
will also form a valid ramp withi1
(sincenums[i1] <= nums[i2] <= nums[j]
) - The width
j - i1
will always be greater thanj - i2
This means we only need to consider indices as potential starting positions if they have values smaller than all previous indices. These indices form a decreasing sequence.
Why use a stack? As we traverse from right to left looking for ending positions:
- When we find a valid ramp with starting position
i
, we can removei
from consideration because any futurej
(to the left) would give a smaller width - The stack structure allows us to efficiently check and remove these starting positions in order
The algorithm becomes:
- Build a stack of indices with decreasing values (potential optimal starting points)
- Traverse from right to left, and for each position, pop all valid starting positions from the stack while updating the maximum width
- Once a starting position is used, it's removed since any other ending position would yield a smaller width
Learn more about Stack, Two Pointers and Monotonic Stack patterns.
Solution Approach
The solution uses a monotonic decreasing stack to efficiently find the maximum width ramp.
Step 1: Build the Monotonic Stack
stk = []
for i, v in enumerate(nums):
if not stk or nums[stk[-1]] > v:
stk.append(i)
We iterate through the array from left to right and build a stack that stores indices. An index i
is added to the stack only if:
- The stack is empty (first element)
nums[i] < nums[stk[-1]]
(current value is smaller than the value at the top of the stack)
This creates a stack where the indices correspond to strictly decreasing values. For example, if nums = [6, 0, 8, 2, 1, 5]
, the stack would contain indices [0, 1, 4]
corresponding to values [6, 0, 1]
.
Step 2: Find Maximum Width by Scanning Right to Left
ans = 0
for i in range(len(nums) - 1, -1, -1):
while stk and nums[stk[-1]] <= nums[i]:
ans = max(ans, i - stk.pop())
if not stk:
break
We traverse the array from right to left (from index n-1
to 0
). For each position i
:
- While the stack is not empty and
nums[stk[-1]] <= nums[i]
, we have found a valid ramp - We calculate the width as
i - stk[-1]
and update our answer - We pop the index from the stack because any future ending position (to the left of current
i
) would give a smaller width for this starting position - If the stack becomes empty, we can break early since we've found ramps for all potential starting positions
Why This Works:
- The monotonic stack ensures we only consider the most promising starting positions (leftmost positions with each distinct "low" value)
- Processing from right to left ensures we find the maximum width for each starting position
- Popping used starting positions is optimal because once we've found the rightmost valid ending for a start position, any other ending would be suboptimal
Time Complexity: O(n)
where n
is the length of the array. Each element is pushed and popped from the stack at most once.
Space Complexity: O(n)
in the worst case where the array is strictly decreasing, causing all indices to be stored in the stack.
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 the algorithm with nums = [6, 0, 8, 2, 1, 5]
.
Step 1: Build the monotonic decreasing stack
We iterate through the array and add indices where values are strictly decreasing:
- i=0, v=6: Stack is empty, add 0. Stack:
[0]
- i=1, v=0: 0 < 6, add 1. Stack:
[0, 1]
- i=2, v=8: 8 > 0, skip. Stack:
[0, 1]
- i=3, v=2: 2 > 0, skip. Stack:
[0, 1]
- i=4, v=1: 1 > 0, skip. Stack:
[0, 1]
- i=5, v=5: 5 > 0, skip. Stack:
[0, 1]
Final stack: [0, 1]
(indices with values [6, 0])
Step 2: Scan from right to left to find maximum width
Start with ans = 0
.
-
i=5, nums[5]=5:
- Check stack top: nums[1]=0 ≤ 5 ✓
- Width = 5 - 1 = 4, update ans = 4
- Pop index 1. Stack:
[0]
- Check stack top: nums[0]=6 > 5 ✗
- Continue
-
i=4, nums[4]=1:
- Check stack top: nums[0]=6 > 1 ✗
- Continue
-
i=3, nums[3]=2:
- Check stack top: nums[0]=6 > 2 ✗
- Continue
-
i=2, nums[2]=8:
- Check stack top: nums[0]=6 ≤ 8 ✓
- Width = 2 - 0 = 2, ans remains 4 (4 > 2)
- Pop index 0. Stack:
[]
- Stack is empty, break early
Result: Maximum width = 4 (from indices 1 to 5)
The algorithm efficiently found that the widest ramp is from index 1 (value 0) to index 5 (value 5), giving a width of 4. By using the monotonic stack, we avoided checking unnecessary combinations and found the optimal solution in linear time.
Solution Implementation
1class Solution:
2 def maxWidthRamp(self, nums: List[int]) -> int:
3 # Build a decreasing monotonic stack of indices
4 # This stack will contain indices where values form a decreasing sequence
5 stack = []
6
7 # First pass: build the decreasing stack from left to right
8 for index, value in enumerate(nums):
9 # Only add index if stack is empty or current value is smaller than stack top
10 # This ensures we have potential left boundaries for ramps
11 if not stack or nums[stack[-1]] > value:
12 stack.append(index)
13
14 max_width = 0
15
16 # Second pass: traverse from right to left to find maximum width ramps
17 for right_index in range(len(nums) - 1, -1, -1):
18 # While stack has indices and current value is >= value at stack top
19 # We found a valid ramp, calculate its width
20 while stack and nums[stack[-1]] <= nums[right_index]:
21 left_index = stack.pop()
22 max_width = max(max_width, right_index - left_index)
23
24 # Early termination: if stack is empty, we've found all possible ramps
25 if not stack:
26 break
27
28 return max_width
29
1class Solution {
2 public int maxWidthRamp(int[] nums) {
3 int n = nums.length;
4 // Stack to store indices of potential left boundaries (in decreasing order of values)
5 Deque<Integer> stack = new ArrayDeque<>();
6
7 // Build a decreasing stack of indices from left to right
8 // These indices represent potential left boundaries of the ramp
9 for (int i = 0; i < n; i++) {
10 // Only add index if stack is empty or current value is smaller than stack top
11 // This ensures we have all possible left boundaries in decreasing order
12 if (stack.isEmpty() || nums[stack.peek()] > nums[i]) {
13 stack.push(i);
14 }
15 }
16
17 int maxWidth = 0;
18
19 // Traverse from right to left to find maximum width ramps
20 for (int i = n - 1; i >= 0; i--) {
21 // Pop from stack while current value is >= stack top value
22 // This forms valid ramps (nums[stack.peek()] <= nums[i])
23 while (!stack.isEmpty() && nums[stack.peek()] <= nums[i]) {
24 // Update maximum width with current ramp width (i - j)
25 maxWidth = Math.max(maxWidth, i - stack.pop());
26 }
27
28 // Early termination: if stack is empty, we've found all possible ramps
29 if (stack.isEmpty()) {
30 break;
31 }
32 }
33
34 return maxWidth;
35 }
36}
37
1class Solution {
2public:
3 int maxWidthRamp(vector<int>& nums) {
4 int n = nums.size();
5
6 // Build a monotonic decreasing stack of indices
7 // Stack stores indices where values form a decreasing sequence
8 stack<int> decreasingStack;
9
10 // First pass: build the decreasing stack from left to right
11 // Only push index i if nums[i] is smaller than the top element
12 // This gives us potential left boundaries for ramps
13 for (int i = 0; i < n; ++i) {
14 if (decreasingStack.empty() || nums[decreasingStack.top()] > nums[i]) {
15 decreasingStack.push(i);
16 }
17 }
18
19 int maxWidth = 0;
20
21 // Second pass: traverse from right to left to find maximum width ramps
22 // For each position i from right, check if it can form a valid ramp
23 // with indices in the stack (where nums[stackIndex] <= nums[i])
24 for (int i = n - 1; i >= 0; --i) {
25 // Pop all indices from stack that can form a valid ramp with current i
26 // Since we traverse from right to left, this ensures maximum width
27 while (!decreasingStack.empty() && nums[decreasingStack.top()] <= nums[i]) {
28 maxWidth = max(maxWidth, i - decreasingStack.top());
29 decreasingStack.pop();
30 }
31
32 // Early termination: if stack is empty, we've found all possible ramps
33 if (decreasingStack.empty()) {
34 break;
35 }
36 }
37
38 return maxWidth;
39 }
40};
41
1/**
2 * Finds the maximum width of a ramp in the array.
3 * A ramp is a pair (i, j) where i < j and nums[i] <= nums[j].
4 * The width of such a ramp is j - i.
5 *
6 * @param nums - The input array of numbers
7 * @returns The maximum width of a ramp, or 0 if no valid ramp exists
8 */
9function maxWidthRamp(nums: number[]): number {
10 let maxWidth: number = 0;
11 const arrayLength: number = nums.length;
12 const monotonicStack: number[] = [];
13
14 // Build a decreasing monotonic stack of indices
15 // Only store indices where values form a decreasing sequence
16 for (let i = 0; i < arrayLength - 1; i++) {
17 if (monotonicStack.length === 0 || nums[monotonicStack[monotonicStack.length - 1]] > nums[i]) {
18 monotonicStack.push(i);
19 }
20 }
21
22 // Traverse from right to left to find maximum width ramps
23 for (let j = arrayLength - 1; j >= 0; j--) {
24 // Pop indices from stack while current value can form a valid ramp
25 while (monotonicStack.length > 0 && nums[monotonicStack[monotonicStack.length - 1]] <= nums[j]) {
26 const leftIndex: number = monotonicStack.pop()!;
27 maxWidth = Math.max(maxWidth, j - leftIndex);
28 }
29
30 // Early termination if stack is empty
31 if (monotonicStack.length === 0) {
32 break;
33 }
34 }
35
36 return maxWidth;
37}
38
Time and Space Complexity
Time Complexity: O(n)
where n
is the length of the input array nums
.
- The first loop iterates through all elements once to build the monotonic decreasing stack, taking
O(n)
time. - The second loop iterates through the array from right to left once. While there's a nested while loop, each element in the stack is popped at most once throughout the entire execution, making the amortized time complexity
O(n)
. - Overall, the total time complexity is
O(n) + O(n) = O(n)
.
Space Complexity: O(n)
in the worst case.
- The stack
stk
stores indices of elements from the input array. - In the worst case, when the array is strictly decreasing (e.g.,
[5, 4, 3, 2, 1]
), all indices will be pushed onto the stack, requiringO(n)
space. - No other auxiliary data structures are used that scale with input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Building an Increasing Stack Instead of Decreasing
The Mistake: A common error is accidentally building an increasing monotonic stack by using the wrong comparison operator:
# WRONG: This builds an increasing stack if not stack or nums[stack[-1]] < value: # Should be > stack.append(index)
This fundamentally breaks the algorithm because:
- We need indices with smaller values as potential starting points for ramps
- An increasing stack would store indices with larger values, missing valid ramp starts
The Fix: Ensure you're using the correct comparison to build a decreasing stack:
# CORRECT: This builds a decreasing stack if not stack or nums[stack[-1]] > value: stack.append(index)
Pitfall 2: Not Understanding Why We Traverse Right to Left
The Mistake: Some might attempt to traverse left to right in the second phase:
# WRONG: Traversing left to right
for i in range(len(nums)):
while stack and nums[stack[-1]] <= nums[i]:
ans = max(ans, i - stack.pop())
This fails because:
- We want the maximum width for each starting position
- Going left to right would find the leftmost valid ending, not the rightmost
- This gives suboptimal widths
The Fix: Always traverse from right to left to ensure maximum width:
# CORRECT: Right to left traversal
for i in range(len(nums) - 1, -1, -1):
while stack and nums[stack[-1]] <= nums[i]:
ans = max(ans, i - stack.pop())
Pitfall 3: Forgetting the Early Termination Optimization
The Mistake: Omitting the early break when the stack becomes empty:
# INEFFICIENT: Missing early termination
for i in range(len(nums) - 1, -1, -1):
while stack and nums[stack[-1]] <= nums[i]:
ans = max(ans, i - stack.pop())
# Missing: if not stack: break
While this still produces correct results, it unnecessarily continues iterating even after all potential starting positions have been processed.
The Fix: Add the early termination check:
# OPTIMIZED: With early termination
for i in range(len(nums) - 1, -1, -1):
while stack and nums[stack[-1]] <= nums[i]:
ans = max(ans, i - stack.pop())
if not stack: # All starting positions processed
break
Pitfall 4: Incorrectly Handling the Comparison in the Second Phase
The Mistake: Using strict inequality instead of less than or equal:
# WRONG: Using strict inequality
while stack and nums[stack[-1]] < nums[i]: # Should be <=
ans = max(ans, i - stack.pop())
This misses valid ramps where nums[i] == nums[j]
, which are allowed by the problem definition.
The Fix: Use the correct comparison operator:
# CORRECT: Using <=
while stack and nums[stack[-1]] <= nums[i]:
ans = max(ans, i - stack.pop())
Depth first search is equivalent to which of the tree traversal order?
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
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
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!