Monotonic Stack
Scope — Next greater / previous smaller / span / histogram problems — the stack stays sorted so each element is pushed and popped once. See also: stack.md — plain LIFO problems; monotonic_queue.md — the sliding-window counterpart; heap.md — when you need a global extreme instead of a neighbouring one; greedy.md — where the append-only “stacks” of §2-10 really belong.
LeetCode Problem Lists
Overview
Monotonic Stack is a specialized stack data structure that maintains a monotonic (either strictly increasing or strictly decreasing) order of elements. It efficiently solves problems related to finding next greater/smaller elements, histogram areas, and sequence optimization problems.
Key Properties
- Time Complexity: O(n) for most operations (each element pushed/popped once)
- Space Complexity: O(n) for the stack storage
- Core Idea: Maintain monotonic order while processing elements sequentially
- When to Use: Finding next/previous greater/smaller elements, histogram problems, sequence optimization
References
Problem Categories
Pattern 1: Next/Previous Greater Element — LC 739
- Description: Find the next or previous element that is greater than current element
- Examples: LC 496 (Next Greater Element I), LC 503 (Next Greater Element II), LC 739 (Daily Temperatures)
- Pattern: Use decreasing monotonic stack, pop when finding greater element
Pattern 2: Next/Previous Smaller Element — LC 84
- Description: Find the next or previous element that is smaller than current element
- Examples: LC 84 (Largest Rectangle), LC 42 (Trapping Rain Water), LC 907 (Sum of Subarray Minimums)
- Pattern: Use increasing monotonic stack, pop when finding smaller element
Pattern 3: Histogram and Area Problems — LC 84
- Description: Calculate areas, rectangles, or volumes using height information
- Examples: LC 84 (Largest Rectangle in Histogram), LC 42 (Trapping Rain Water), LC 85 (Maximal Rectangle)
- Pattern: Find boundaries using monotonic stack, calculate areas between boundaries
Pattern 4: Sequence Order and Validation — LC 456
- Description: Validate sequences, find patterns, or maintain order constraints
- Examples: LC 456 (132 Pattern), LC 901 (Online Stock Span), LC 1856 (Maximum Subarray Min-Product)
- Pattern: Use stack to maintain sequence properties and validate patterns
Pattern 5: Optimization and Maximum/Minimum — LC 1793
- Description: Find optimal solutions involving maximum or minimum constraints
- Examples: LC 1944 (Number of Visible People), LC 2104 (Sum of Subarray Ranges), LC 1793 (Maximum Score)
- Pattern: Use monotonic properties to maintain optimal candidates
Pattern 6: Circular Arrays — LC 503
- Description: Handle circular or cyclic array problems
- Examples: LC 503 (Next Greater Element II), LC 457 (Circular Array Loop)
- Pattern: Process array twice or use modular arithmetic with monotonic stack
Templates & Algorithms
Template Comparison Table
| Template Type | Use Case | Stack Order | When to Use |
|---|---|---|---|
| Decreasing Stack | Next/Previous Greater | Decreasing | Find elements greater than current |
| Increasing Stack | Next/Previous Smaller | Increasing | Find elements smaller than current |
| Histogram Area | Rectangle/Area Problems | Increasing | Calculate areas using heights |
| Circular Array | Cyclic Problems | Varies | Process circular sequences |
| Pattern Validation | Sequence Validation | Varies | Validate specific patterns |
| Optimization Stack | Max/Min Problems | Varies | Maintain optimal candidates |
Universal Template
def monotonic_stack_template(arr):
"""
Universal template for monotonic stack problems
Modify the condition and processing logic based on problem requirements
"""
stack = [] # Store indices or values
result = []
for i, val in enumerate(arr):
# Pop elements that violate monotonic property
while stack and should_pop(stack, val, i):
# Process the popped element
popped = stack.pop()
process_popped_element(popped, i, result)
# Add current element to stack
stack.append(i) # or val depending on problem
# Process remaining elements in stack
while stack:
popped = stack.pop()
process_remaining_element(popped, result)
return result
def should_pop(stack, current_val, current_idx):
"""Define when to pop based on problem requirements"""
# For next greater: return arr[stack[-1]] <= current_val
# For next smaller: return arr[stack[-1]] >= current_val
pass
def process_popped_element(popped_idx, current_idx, result):
"""Process element when it's popped (found its next greater/smaller)"""
pass
def process_remaining_element(popped_idx, result):
"""Process elements remaining in stack at the end"""
pass
// Java Universal Template
public int[] monotonicStackTemplate(int[] arr) {
Stack<Integer> stack = new Stack<>();
int[] result = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
// Pop elements that violate monotonic property
while (!stack.isEmpty() && shouldPop(stack, arr, i)) {
int poppedIdx = stack.pop();
processElement(poppedIdx, i, result, arr);
}
// Add current element to stack
stack.push(i);
}
// Process remaining elements
while (!stack.isEmpty()) {
int poppedIdx = stack.pop();
processRemainingElement(poppedIdx, result);
}
return result;
}
private boolean shouldPop(Stack<Integer> stack, int[] arr, int currentIdx) {
// Define condition based on problem requirements
return arr[stack.peek()] <= arr[currentIdx]; // For next greater
}
Template 1: Next Greater Element (Decreasing Stack) — LC 496
def next_greater_element(nums):
"""
Find next greater element for each element
LC 496, LC 503, LC 739
"""
n = len(nums)
result = [-1] * n
stack = [] # Store indices
for i in range(n):
# Pop smaller or equal elements
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i] # Found next greater
stack.append(i)
return result
// Java Template 1
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
result[stack.pop()] = nums[i];
}
stack.push(i);
}
return result;
}
Template 2: Next Smaller Element (Increasing Stack) — LC 84
def next_smaller_element(nums):
"""
Find next smaller element for each element
Used in LC 84, LC 42
"""
n = len(nums)
result = [-1] * n
stack = [] # Store indices
for i in range(n):
# Pop greater or equal elements
while stack and nums[stack[-1]] > nums[i]:
idx = stack.pop()
result[idx] = nums[i] # Found next smaller
stack.append(i)
return result
Template 3: Largest Rectangle in Histogram — LC 84
def largest_rectangle_area(heights):
"""
Find largest rectangle area in histogram
LC 84, LC 85
"""
stack = [] # Store indices
max_area = 0
heights.append(0) # Add sentinel
for i, h in enumerate(heights):
# Pop taller bars and calculate area
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
// Java Template 3
public int largestRectangleArea(int[] heights) {
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
int n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
Template 4: Circular Array Processing — LC 503
def next_greater_circular(nums):
"""
Find next greater element in circular array
LC 503
"""
n = len(nums)
result = [-1] * n
stack = []
# Process array twice to handle circular nature
for i in range(2 * n):
# Pop smaller elements
while stack and nums[stack[-1]] < nums[i % n]:
idx = stack.pop()
result[idx] = nums[i % n]
# Only add indices from first pass
if i < n:
stack.append(i)
return result
Template 5: Stack with Additional Information
def monotonic_stack_with_info(nums):
"""
Store additional information with stack elements
Used for complex calculations
"""
stack = [] # Store (index, value, additional_info)
result = []
for i, val in enumerate(nums):
while stack and stack[-1][1] <= val:
idx, old_val, info = stack.pop()
# Process with additional information
result.append(calculate_result(idx, i, old_val, val, info))
# Calculate additional information for current element
additional_info = calculate_info(val, stack)
stack.append((i, val, additional_info))
return result
Template 6: Pattern Validation (132 Pattern) — LC 456
def find_132_pattern(nums):
"""
Find 132 pattern in array
LC 456
"""
n = len(nums)
if n < 3:
return False
stack = [] # Store potential k values (decreasing)
second = float('-inf') # The "2" in 132 pattern
# Traverse from right to left
for i in range(n - 1, -1, -1):
if nums[i] < second: # Found "1" < "2"
return True
# Pop smaller values and update second
while stack and stack[-1] < nums[i]:
second = stack.pop()
stack.append(nums[i])
return False
Problems by Pattern
Pattern-Based Problem Classification
Pattern 1: Next/Previous Greater Element Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Next Greater Element I | 496 | Decreasing stack | Easy | Template 1 |
| Next Greater Element II | 503 | Circular array | Medium | Template 4 |
| Daily Temperatures | 739 | Distance calculation | Medium | Template 1 |
| Remove K Digits | 402 | Greedy + stack | Medium | Template 1 |
| Remove Duplicate Letters | 316 | Lexicographical + stack | Medium | Template 1 |
| Sliding Window Maximum | 239 | Monotonic deque | Hard | Template 1 |
| Shortest Unsorted Array | 581 | Two-pass stack | Medium | Template 1 |
| Sum of Subarray Ranges | 2104 | Next greater + smaller | Medium | Template 1+2 |
Pattern 2: Next/Previous Smaller Element Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Largest Rectangle in Histogram | 84 | Area calculation | Hard | Template 3 |
| Maximal Rectangle | 85 | 2D histogram | Hard | Template 3 |
| Sum of Subarray Minimums | 907 | Contribution method | Medium | Template 2 |
| Number of Valid Subarrays | 1063 | Smaller element count | Medium | Template 2 |
| Minimum Cost Tree From Leaf Values | 1130 | Optimal merging | Medium | Template 2 |
| Find the Most Competitive Subsequence | 1673 | Subsequence selection | Medium | Template 2 |
| Maximum Subarray Min-Product | 1856 | Min value as pivot | Medium | Template 2 |
Pattern 3: Histogram and Area Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Trapping Rain Water | 42 | Water level calculation | Hard | Template 2 |
| Container With Most Water | 11 | Two pointers alternative | Medium | Template 2 |
| Maximal Rectangle | 85 | Row-wise histogram | Hard | Template 3 |
| Maximum Rectangle | 221 | DP + histogram | Medium | Template 3 |
| Minimum Number of Taps | 1326 | Interval coverage | Hard | Template 2 |
| Constrained Subsequence Sum | 1425 | DP + monotonic deque | Hard | Template 2 |
Pattern 4: Sequence Order and Validation Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| 132 Pattern | 456 | Pattern detection | Medium | Template 6 |
| Online Stock Span | 901 | Monotonic stack | Medium | Template 1 |
| Score of Parentheses | 856 | Nested structure | Medium | Template 5 |
| Valid Parenthesis String | 678 | Balance validation | Medium | Template 5 |
| Minimum Add to Make Parentheses Valid | 921 | Balance counting | Medium | Template 5 |
| Validate Stack Sequences | 946 | Sequence simulation | Medium | Template 5 |
| Maximum Nesting Depth of Parentheses | 1614 | Depth tracking | Easy | Template 5 |
| Minimum Remove to Make Valid Parentheses | 1249 | Balance + removal | Medium | Template 5 |
Pattern 5: Optimization and Maximum/Minimum Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Maximum Score of Good Subarray | 1793 | Two pointers + stack | Hard | Template 2 |
| Number of Visible People in Queue | 1944 | Line of sight | Medium | Template 1 |
| Car Fleet | 853 | Arrival-time running max — the stack never pops | Medium | §2-10 |
| Count Robot Groups | 4045 | Same greedy, no finish line — compare speeds | Medium | §2-10 |
| Car Fleet II | 1776 | Per-car collision time — a real stack, with pops | Hard | §2-10 |
| Buildings With Ocean View | 1762 | Right-to-left scan | Medium | Template 1 |
| Find the Winner of Circular Game | 1823 | Josephus problem | Medium | Template 4 |
| Maximum Width Ramp | 962 | Index difference | Medium | Template 1 |
| Steps to Make Array Non-decreasing | 2289 | Stack carries a dp value | Medium | Template 11 |
| Pancake Sorting | 969 | Reverse operations | Medium | Template 1 |
Pattern 6: Circular Array Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Next Greater Element II | 503 | Double array traversal | Medium | Template 4 |
| Circular Array Loop | 457 | Cycle detection | Medium | Template 4 |
| Design Circular Queue | 622 | Circular buffer | Medium | Template 4 |
| Design Circular Deque | 641 | Double-ended circular | Medium | Template 4 |
Advanced/Mixed Pattern Problems
| Problem | LC # | Key Technique | Difficulty | Template |
|---|---|---|---|---|
| Sum of Total Strength of Wizards | 2281 | Multiple stacks | Hard | Multiple |
| Number of Ways to Rearrange Sticks | 1866 | Combinatorics + stack | Hard | Template 5 |
| Basic Calculator | 224 | Expression evaluation | Hard | Template 5 |
| Basic Calculator II | 227 | Operator precedence | Medium | Template 5 |
| Basic Calculator III | 772 | Full expression parsing | Hard | Template 5 |
| Evaluate Reverse Polish Notation | 150 | Postfix evaluation | Medium | Template 5 |
| Decode String | 394 | Nested decoding | Medium | Template 5 |
| Find Duplicate Subtrees | 652 | Tree serialization | Medium | Template 5 |
| Exclusive Time of Functions | 636 | Call stack simulation | Medium | Template 5 |
| Minimum Window Subsequence | 727 | Two pointers + stack | Hard | Template 5 |
Problem Difficulty Distribution
- Easy (8 problems): Basic next greater/smaller, simple validations
- Medium (28 problems): Most common difficulty, various patterns
- Hard (16 problems): Complex area calculations, advanced optimizations
Template Usage Frequency
- Template 1 (Next Greater): 15 problems
- Template 2 (Next Smaller): 12 problems
- Template 3 (Histogram): 8 problems
- Template 4 (Circular): 6 problems
- Template 5 (Validation/Complex): 11 problems
- Multiple Templates: 8 problems
Pattern Selection Strategy
Decision Framework Flowchart
Problem Analysis for Monotonic Stack:
1. Does the problem involve finding next/previous elements?
├── YES: Next/Previous GREATER elements?
│ ├── YES: Use Template 1 (Decreasing Stack)
│ │ ├── Array is circular? → Use Template 4 (Circular)
│ │ └── Standard case → Template 1
│ └── NO: Next/Previous SMALLER elements?
│ ├── YES: Use Template 2 (Increasing Stack)
│ └── NO: Continue to step 2
└── NO: Continue to step 2
2. Does the problem involve heights/areas/rectangles?
├── YES: Rectangle area calculation?
│ ├── YES: Use Template 3 (Histogram)
│ └── NO: Water trapping/volume?
│ └── YES: Use Template 2 (Next Smaller)
└── NO: Continue to step 3
3. Does the problem involve sequence validation/patterns?
├── YES: Parentheses/brackets?
│ ├── YES: Use Template 5 (Validation)
│ └── NO: Specific pattern (like 132)?
│ └── YES: Use Template 6 (Pattern Detection)
└── NO: Continue to step 4
4. Does the problem involve optimization/max-min constraints?
├── YES: Multiple criteria optimization?
│ ├── YES: Use Template 5 (Complex Info)
│ └── NO: Simple max/min tracking?
│ └── YES: Use Template 1 or 2
└── NO: Continue to step 5
5. Does the problem involve circular arrays or cyclic behavior?
├── YES: Use Template 4 (Circular Processing)
└── NO: Consider if monotonic stack is the right approach
└── May need different data structure/algorithm
Step-by-Step Problem Analysis
-
Identify the Core Requirement
- Next/Previous element queries → Templates 1, 2, 4
- Area/Rectangle calculations → Template 3
- Pattern validation → Templates 5, 6
- Optimization problems → Templates 1, 2, 5
-
Determine Stack Order
- Need greater elements → Decreasing stack (pop smaller)
- Need smaller elements → Increasing stack (pop greater)
- Area calculations → Usually increasing stack
- Pattern detection → Varies by pattern
-
Choose Processing Direction
- Left to right: Most common, natural order
- Right to left: For “next” elements, sometimes easier
- Circular: Process array multiple times
-
Decide What to Store
- Indices: When need position information
- Values: When only need element comparison
- Tuples: When need additional information
Template Selection Quick Guide
| Problem Type | Template | Stack Content | Processing Order |
|---|---|---|---|
| Next Greater | Template 1 | Indices | Left to Right |
| Next Smaller | Template 2 | Indices | Left to Right |
| Previous Greater | Template 1 | Indices | Left to Right |
| Previous Smaller | Template 2 | Indices | Left to Right |
| Histogram Areas | Template 3 | Indices | Left to Right |
| Circular Arrays | Template 4 | Indices | 2x traversal |
| Pattern Detection | Template 6 | Values | Right to Left |
| Complex Validation | Template 5 | Tuples | Varies |
Summary & Quick Reference
Complexity Quick Reference
| Operation | Time | Space | Notes |
|---|---|---|---|
| Push to Stack | O(1) | - | Each element pushed once |
| Pop from Stack | O(1) | - | Each element popped once |
| Overall Algorithm | O(n) | O(n) | Amortized linear time |
| Next Greater/Smaller | O(n) | O(n) | Single pass through array |
| Histogram Area | O(n) | O(n) | Linear scan with stack |
| Circular Array | O(n) | O(n) | Two passes, same complexity |
Template Quick Reference
| Template | Pattern | Key Code Pattern |
|---|---|---|
| Template 1 | Next Greater | while stack and nums[stack[-1]] < nums[i] |
| Template 2 | Next Smaller | while stack and nums[stack[-1]] > nums[i] |
| Template 3 | Histogram | while stack and heights[stack[-1]] > h |
| Template 4 | Circular | for i in range(2 * n) |
| Template 5 | Validation | Store additional info in stack |
| Template 6 | Pattern Detection | Right-to-left with condition tracking |
Common Patterns & Tricks
Next Greater Element Pattern
# Standard next greater element
def next_greater_elements(nums):
stack, result = [], [-1] * len(nums)
for i, num in enumerate(nums):
while stack and nums[stack[-1]] < num:
result[stack.pop()] = num
stack.append(i)
return result
Contribution Method for Subarrays
# Count contribution of each element
def sum_subarray_mins(arr):
n = len(arr)
left = [-1] * n # Previous smaller element
right = [n] * n # Next smaller element
# Calculate left boundaries
stack = []
for i in range(n):
while stack and arr[stack[-1]] >= arr[i]:
stack.pop()
left[i] = stack[-1] if stack else -1
stack.append(i)
# Calculate contribution
result = 0
for i in range(n):
result += arr[i] * (i - left[i]) * (right[i] - i)
return result % (10**9 + 7)
Histogram Area Calculation
# Largest rectangle with height as key
def largest_rectangle_area(heights):
stack = []
max_area = 0
for i, h in enumerate(heights + [0]):
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
Problem-Solving Steps
-
Step 1: Identify Pattern Type
- Look for keywords: next/previous, greater/smaller, area, rectangle
- Check for circular/cyclic requirements
- Identify if validation or pattern detection is needed
-
Step 2: Choose Appropriate Template
- Use decision framework flowchart
- Consider stack order (increasing vs decreasing)
- Determine what information to store in stack
-
Step 3: Implement Core Logic
- Set up stack and result containers
- Implement while loop with correct popping condition
- Process popped elements appropriately
- Handle remaining elements in stack
-
Step 4: Handle Edge Cases
- Empty array
- Single element array
- All elements same
- Strictly increasing/decreasing sequences
-
Step 5: Optimize and Verify
- Ensure O(n) time complexity
- Check space complexity
- Verify with sample inputs
- Handle integer overflow if needed
Common Mistakes & Tips
Common Mistakes
- Wrong Stack Order: Using increasing stack for next greater problems (should be decreasing)
- Index vs Value Confusion: Storing values when indices are needed for distance calculation
- Incomplete Processing: Forgetting to process remaining elements in stack
- Boundary Issues: Not handling empty stack cases properly
- Circular Logic: Not processing circular arrays correctly (missing second pass)
- Condition Errors: Using wrong comparison operators (< vs <=, > vs >=)
Best Practices
- Always store indices when you need position information
- Use sentinel values (like 0) to simplify boundary handling
- Process from left to right unless specifically need right-to-left
- Clear variable names:
stack,result,current_idxinstead ofs,res,i - Comment the while condition to clarify monotonic property
- Handle edge cases first before main algorithm
Interview Tips
-
Pattern Recognition
- Listen for “next greater/smaller” keywords
- Area/rectangle problems often use monotonic stacks
- Sequence validation problems may need stack-based approaches
-
Problem-Solving Approach
- Start with brute force to understand the problem
- Identify if monotonic property can optimize the solution
- Draw examples to visualize stack behavior
-
Communication During Interview
- Explain why monotonic stack is appropriate
- Walk through the stack state with examples
- Discuss time/space complexity trade-offs
-
Implementation Tips
- Start with the template structure
- Focus on getting the while condition right
- Test with simple examples (like [2,1,2,4,3,1])
-
Follow-up Questions to Expect
- How to handle duplicates?
- What if we need previous instead of next?
- Can you optimize space complexity?
- How to extend to 2D problems?
Related Topics
- Stack: Monotonic stack is a specialized application of stack data structure
- Deque: Monotonic deque for sliding window maximum problems
- Two Pointers: Alternative approach for some area calculation problems
- Dynamic Programming: Some optimization problems combine DP with monotonic stacks
- Binary Search: Finding boundaries in sorted structures
- Segment Tree: Advanced queries on range maximum/minimum
LC Examples
2-1) Daily Temperatures (LC 739) — Monotonic Decreasing Stack
Stack stores indices; pop when a warmer day is found.
// LC 739 - Daily Temperatures
// IDEA: Monotonic decreasing stack — pop when current > stack top
// time = O(N), space = O(N)
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
Deque<Integer> stack = new ArrayDeque<>(); // stores indices
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
int idx = stack.pop();
ans[idx] = i - idx;
}
stack.push(i);
}
return ans;
}
2-2) Largest Rectangle in Histogram (LC 84) — Monotonic Increasing Stack
Pop a bar when a shorter bar arrives; compute area using width from stack.
// LC 84 - Largest Rectangle in Histogram
// IDEA: Monotonic increasing stack — pop and compute area on shorter bar
// time = O(N), space = O(N)
public int largestRectangleArea(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
int maxArea = 0;
for (int i = 0; i <= heights.length; i++) {
int h = (i == heights.length) ? 0 : heights[i];
while (!stack.isEmpty() && h < heights[stack.peek()]) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
2-3) Next Greater Element I (LC 496) — Monotonic Stack + HashMap
Precompute next greater element for nums2, then answer queries for nums1.
// LC 496 - Next Greater Element I
// IDEA: Monotonic decreasing stack on nums2; store results in map
// time = O(M + N), space = O(M)
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer, Integer> map = new HashMap<>(); // val -> next greater val
Deque<Integer> stack = new ArrayDeque<>();
for (int num : nums2) {
while (!stack.isEmpty() && num > stack.peek()) {
map.put(stack.pop(), num);
}
stack.push(num);
}
int[] ans = new int[nums1.length];
for (int i = 0; i < nums1.length; i++) {
ans[i] = map.getOrDefault(nums1[i], -1);
}
return ans;
}
2-4) Trapping Rain Water (LC 42) — Monotonic Stack
Pop a bar when a taller bar arrives; water trapped = (min height difference) * width.
// LC 42 - Trapping Rain Water
// IDEA: Monotonic stack — pop when taller bar found, water fills between boundaries
// time = O(N), space = O(N)
public int trap(int[] height) {
Deque<Integer> stack = new ArrayDeque<>();
int water = 0;
for (int i = 0; i < height.length; i++) {
while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
int bottom = stack.pop();
if (stack.isEmpty()) break;
int left = stack.peek();
int width = i - left - 1;
int boundedHeight = Math.min(height[left], height[i]) - height[bottom];
water += width * boundedHeight;
}
stack.push(i);
}
return water;
}
2-5) Next Greater Element II (LC 503) — Circular Monotonic Stack
Process array twice (or use modulo) to handle circular next-greater queries.
// LC 503 - Next Greater Element II (circular array)
// IDEA: Monotonic stack — traverse 2n indices with modulo for circular effect
// time = O(N), space = O(N)
public int[] nextGreaterElements(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, -1);
Deque<Integer> stack = new ArrayDeque<>();
for (int i = 0; i < 2 * n; i++) {
while (!stack.isEmpty() && nums[i % n] > nums[stack.peek()]) {
ans[stack.pop()] = nums[i % n];
}
if (i < n) stack.push(i);
}
return ans;
}
2-6) Online Stock Span (LC 901) — Monotonic Decreasing Stack
Pop all previous prices <= current; span = days since last greater price.
// LC 901 - Online Stock Span
// IDEA: Monotonic decreasing stack storing [price, span] pairs
// time = O(1) amortized per call, space = O(N)
class StockSpanner {
Deque<int[]> stack = new ArrayDeque<>(); // [price, span]
public int next(int price) {
int span = 1;
while (!stack.isEmpty() && stack.peek()[0] <= price) {
span += stack.pop()[1];
}
stack.push(new int[]{price, span});
return span;
}
}
2-7) Sum of Subarray Minimums (LC 907) — Monotonic Stack
For each element, find left/right boundaries where it is the minimum; use monotonic stack.
// LC 907 - Sum of Subarray Minimums
// IDEA: Monotonic stack — for each element find left & right span as minimum
// time = O(N), space = O(N)
public int sumSubarrayMins(int[] arr) {
int n = arr.length;
int MOD = 1_000_000_007;
int[] left = new int[n], right = new int[n];
Deque<Integer> stack = new ArrayDeque<>();
// left[i] = distance to previous smaller element
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) stack.pop();
left[i] = stack.isEmpty() ? i + 1 : i - stack.peek();
stack.push(i);
}
stack.clear();
// right[i] = distance to next smaller or equal element
for (int i = n-1; i >= 0; i--) {
while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) stack.pop();
right[i] = stack.isEmpty() ? n - i : stack.peek() - i;
stack.push(i);
}
long ans = 0;
for (int i = 0; i < n; i++) ans = (ans + (long) arr[i] * left[i] * right[i]) % MOD;
return (int) ans;
}
Contribution Method — Visualizing left[i] / right[i] (Python) Priority 5 of 5 — Must know — expect it in almost every loop
leetcode_python/Math/sum-of-subarray-minimums.py
Core idea: every subarray has exactly one minimum, so instead of enumerating subarrays we ask “for how many subarrays is arr[i] the minimum?” — then sum arr[i] * count.
For each index i, that count splits into two independent choices:
left choices right choices
<----> <----->
┌───────────────────────────────────────────────┐
│ ... PSE . . . [i] . . . NSE │ arr
└───────────────────────────────────────────────┘
^ ^
previous smaller next smaller-or-equal
element (strict >=) element (strict >)
left[i] = i - PSE ← # of left endpoints that keep arr[i] as min
right[i] = NSE - i ← # of right endpoints that keep arr[i] as min
count(i) = left[i] * right[i]
contribution = arr[i] * left[i] * right[i]
- A subarray keeps
arr[i]as its minimum only if it starts somewhere in(PSE, i]and ends somewhere in[i, NSE). - The two ranges are independent → multiply them.
Handling duplicates (avoid double counting): use >= on the left pass and > on the right pass (asymmetric). Equal values are then counted on exactly one side.
# python
# LC 907 - Sum of Subarray Minimums (contribution method)
# time = O(n), space = O(n)
MOD = 10**9 + 7
n = len(arr)
left = [0] * n # left[i] = distance to previous smaller element
right = [0] * n # right[i] = distance to next smaller-or-equal element
# --- LEFT pass: distance to Previous Smaller Element (pop on >=) ---
mono_st = []
for i in range(n):
val = arr[i]
# Pop elements that are greater than OR EQUAL to current val
while mono_st and arr[mono_st[-1]] >= val:
mono_st.pop() # these can't be the left boundary of arr[i]
# If stack empty -> val is the smallest so far, boundary is index -1
# left choices = i - (-1) = i + 1
# Else -> boundary is the surviving stack top (the PSE)
# left choices = i - mono_st[-1]
left[i] = i + 1 if not mono_st else i - mono_st[-1]
mono_st.append(i)
# --- RIGHT pass: distance to Next Smaller Element (pop on >) ---
mono_st = []
for i in range(n - 1, -1, -1):
val = arr[i]
while mono_st and arr[mono_st[-1]] > val: # strict > here
mono_st.pop()
right[i] = n - i if not mono_st else mono_st[-1] - i
mono_st.append(i)
ans = 0
for i in range(n):
ans = (ans + arr[i] * left[i] * right[i]) % MOD
Why left[i] = i + 1 when the stack is empty: an empty stack means nothing to the left is smaller than arr[i] — arr[i] dominates the whole prefix. The imaginary left boundary sits at index -1, so the left choices span indices 0..i, i.e. i - (-1) = i + 1.
Visual trace on arr = [3, 1, 2, 4]:
i=0 val=3 : stack empty -> left[0] = 0-(-1) = 1 stack=[0]
i=1 val=1 : arr[0]=3 >= 1 -> pop 0
stack empty -> left[1] = 1-(-1) = 2 stack=[1]
i=2 val=2 : arr[1]=1 >= 2? no -> left[2] = 2-1 = 1 stack=[1,2]
i=3 val=4 : arr[2]=2 >= 4? no -> left[3] = 3-2 = 1 stack=[1,2,3]
left = [1, 2, 1, 1]
right = [1, 3, 2, 1] (symmetric backward pass with strict >)
contribution = 3*1*1 + 1*2*3 + 2*1*2 + 4*1*1 = 3 + 6 + 4 + 4 = 17 ✓
2-8) Remove K Digits (LC 402) — Monotonic Increasing Stack
Maintain increasing stack; remove digits when a smaller digit arrives.
// LC 402 - Remove K Digits
// IDEA: Greedy + monotonic increasing stack — remove larger digits greedily
// time = O(N), space = O(N)
public String removeKdigits(String num, int k) {
Deque<Character> stack = new ArrayDeque<>();
for (char c : num.toCharArray()) {
while (k > 0 && !stack.isEmpty() && stack.peek() > c) {
stack.pop(); k--;
}
stack.push(c);
}
while (k-- > 0) stack.pop(); // remove from top if k still > 0
// reconstruct result in correct order (bottom to top of stack)
Deque<Character> result = new ArrayDeque<>(stack);
StringBuilder sb = new StringBuilder();
boolean leadingZero = true;
while (!result.isEmpty()) {
char c = result.pollFirst();
if (leadingZero && c == '0') continue;
leadingZero = false;
sb.append(c);
}
return sb.length() == 0 ? "0" : sb.toString();
}
2-9) Maximal Rectangle (LC 85) — Histogram + Monotonic Stack
For each row, compute histogram heights; apply LC 84 largest rectangle logic per row.
// LC 85 - Maximal Rectangle
// IDEA: For each row build histogram; apply largestRectangleArea (LC 84) logic
// time = O(M*N), space = O(N)
public int maximalRectangle(char[][] matrix) {
if (matrix.length == 0) return 0;
int n = matrix[0].length, maxArea = 0;
int[] heights = new int[n];
for (char[] row : matrix) {
for (int j = 0; j < n; j++)
heights[j] = row[j] == '0' ? 0 : heights[j] + 1;
maxArea = Math.max(maxArea, largestRectangle(heights));
}
return maxArea;
}
private int largestRectangle(int[] heights) {
Deque<Integer> stack = new ArrayDeque<>();
int max = 0;
for (int i = 0; i <= heights.length; i++) {
int h = i == heights.length ? 0 : heights[i];
while (!stack.isEmpty() && h < heights[stack.peek()]) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
max = Math.max(max, height * width);
}
stack.push(i);
}
return max;
}
2-10) The Car Fleet Family (LC 853 / 4045 / 1776) — When a “Stack” Is Really a Greedy Priority 4 of 5 — High value — a gap here costs you rounds
leetcode_python/Stack/car-fleet.py,leetcode_python/Stack/count-robot-groups.py
The section in this file with a negative lesson. LC 853 is taught as a stack problem and tagged
**stack**in this repo’s README — but its stack never pops, so it is a two-variable greedy in a stack costume. Of the three problems here only LC 1776 needs a real stack. Being able to say which, and why, is the whole signal.
The property all three share: the front dominates
nobody can pass the entity ahead of it
-> you can only ever merge with the entity DIRECTLY ahead
-> a merged group moves at the FRONT member's speed (the rear one slows down)
so: scan RIGHT -> LEFT (front -> back), and the survivors appear in order
That last line is why a single “frontmost survivor so far” variable is enough in 853 and 4045: a survivor is never revisited once found, and a stack is only worth its name when you have to go back to an older candidate.
LC 853 Car Fleet — the stack that never pops
There is a finish line at target, so each car collapses to one number — the time it would arrive if nothing were in its way:
t_i = (target - position[i]) / speed[i]
A car joins the fleet ahead iff it would arrive no later than that fleet, i.e. t_i <= max(t of everything ahead of it). So sort front-first and count how many times a new maximum appears:
# python
# LC 853 - Car Fleet (the textbook "stack" version)
# time = O(n log n), space = O(n)
def carFleet(target, position, speed):
st = []
for p, s in sorted(zip(position, speed), reverse=True): # front -> back
t = (target - p) / s
if not st or t > st[-1]: # cannot catch the fleet ahead -> new fleet
st.append(t)
# else: t <= st[-1], it catches up and is absorbed — nothing to record
return len(st)
Look at what that loop never does: it never pops. So st is increasing, st[-1] is simply the largest t seen so far, and len(st) is simply how many times that maximum went up. Two variables do the same job:
# python
# LC 853 - the same algorithm, stack removed
# time = O(n log n) (the sort), space = O(1)
def carFleet(target, position, speed):
cnt, mx = 0, 0.0
for p, s in sorted(zip(position, speed), reverse=True): # front -> back
t = (target - p) / s
if t > mx: # arrives later than anyone ahead -> its own fleet
cnt += 1
mx = t
return cnt
// java
// LC 853 - Car Fleet, running-max greedy
// time = O(N log N), space = O(N) for the index array (O(1) beyond the sort)
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; i++) idx[i] = i;
Arrays.sort(idx, (a, b) -> position[b] - position[a]); // front -> back
int cnt = 0;
double mx = 0.0;
for (int i : idx) {
double t = (double) (target - position[i]) / speed[i];
if (t > mx) { cnt++; mx = t; }
}
return cnt;
}
Both versions agree on random inputs, as they must — they are the same algorithm. Keep whichever you can write under pressure; just do not claim the stack is doing any work.
LC 4045 Count Robot Groups — the same greedy, with no finish line
Robots move forever and there is a merge threshold distance. No finish line means no arrival time exists, so there is no single number per robot to compare — the state is the pair (position, speed) of a group’s rightmost robot. Robot i is absorbed if either test fires:
position[i+1] - position[i] <= distance -> already touching at t = 0
speed[i] > cur_s -> closing on the front group, and with
infinite time ANY positive closing rate
eventually eats ANY gap
# python
# LC 4045 - Count Robot Groups (position is given sorted, so no sort is needed)
# IDEA: right -> left; `cur_s` is the speed of the frontmost group that survived.
# time = O(n), space = O(1)
def countGroups(position, speed, distance):
n = len(position)
cur_s = speed[n - 1] # the frontmost robot is always a group on its own
cnt = 1
for i in range(n - 2, -1, -1):
# touching test -> the NEIGHBOUR; closing test -> the FRONT group
if position[i + 1] - position[i] <= distance or speed[i] > cur_s:
continue # absorbed
cnt += 1
cur_s = speed[i] # a new frontmost survivor
return cnt
The trap: the two tests use different references. A merge adopts the rightmost robot’s state, so cur does not move when someone joins from behind — but the t = 0 merges are simultaneous and chain through neighbours:
position = [18,19,22,24], distance = 3 gaps: 1, 3, 2 -> every pair touches
all four collapse at t = 0, even though 18 is 6 away from the group's position 24
Test the t = 0 touch against cur_p instead of position[i+1] and those chains vanish silently. The speed test is safe against cur on its own: if robot i+1 survived only because it was faster than the front, and i is faster still, then i is faster than the front too — catching your neighbour implies catching the front group.
853 vs 4045 side by side
| LC 853 Car Fleet | LC 4045 Count Robot Groups | |
|---|---|---|
| Horizon | finish line at target |
none — robots run forever |
| Per-entity state | one scalar: t = (target - p) / s |
the pair (position, speed); no scalar exists |
| Merge test | t_i <= max(t ahead) — compare times |
speed[i] > cur_s — compare speeds |
| Merge threshold | bumper to bumper (gap 0) | gap <= distance, plus the t = 0 touch rule |
| Input order | positions unsorted → must sort | position given sorted → no sort |
| Merged group takes | the slower (front) car’s speed | the rightmost robot’s position and speed |
| Cost | O(n log n) / O(1) beyond the sort | O(n) / O(1) |
| Stack needed? | no — append-only | no — two variables |
Why 853 gets a scalar and 4045 does not is the one idea to carry away:
- 853 has a deadline, so “does the rear catch the front?” becomes “does it arrive no later?” — a comparison of one number per car. Two cars can be closing on each other all the way and still be separate fleets, because they would have met past
target. The deadline is what creates the scalar, and the scalar is what makes the running max work. - 4045 has no deadline, so a closing rate is all that matters: given infinite time,
speed[i] > cur_salready is the answer — there is no arithmetic to do, because nobody ever arrives.
LC 1776 Car Fleet II — the member that needs a real stack
Same road, same “front dominates” rule, but the question changes: report the collision time for every car, not a count. That breaks the single-survivor invariant, because the car ahead is itself absorbed at a known time ans[j]:
if car i reaches car j LATER than j's own collision time
-> j is already gone when i gets there
-> i's real target is whatever is further ahead
-> back up to an older candidate <- THIS is a pop
# python
# LC 1776 - Car Fleet II: ans[i] = when car i collides with the car ahead (-1 = never)
# IDEA: right -> left monotonic stack of cars still catchable. Pop a candidate that
# cannot be caught, or that dies before we reach it.
# time = O(n), space = O(n)
def getCollisionTimes(cars):
n = len(cars)
ans = [-1.0] * n
st = [] # indices, front -> back
for i in range(n - 1, -1, -1):
p, s = cars[i]
while st:
j = st[-1]
pj, sj = cars[j]
# (a) not faster than j -> can never reach it
# (b) reaching j takes longer than j survives -> aim further ahead
if s <= sj or (ans[j] > 0 and (pj - p) / (s - sj) >= ans[j]):
st.pop()
else:
break
if st:
j = st[-1]
ans[i] = (cars[j][0] - p) / (s - cars[j][1])
st.append(i)
return ans
# cars = [[1,2],[2,1],[4,3],[7,2]] -> [1.0, -1.0, 3.0, -1.0]
# cars = [[3,4],[5,4],[6,3],[9,1]] -> [2.0, 1.0, 1.5, -1.0]
The rule to take into the room
| You are asked for | Front’s behaviour is | Structure |
|---|---|---|
| a count of surviving groups (853, 4045) | permanent — a survivor is never revisited | one or two variables (greedy) |
| a per-element time or value (1776, 2289) | temporary — the front itself dies at a known time | a real stack, with pops |
So the honest one-liner for an interviewer: “853 and 4045 are the same right-to-left greedy — the usual stack solution for 853 never pops, so I will keep the running maximum instead. 1776 is where the stack earns its place, because an already-computed answer can be invalidated.”
2-11) Asteroid Collision (LC 735) — Stack Simulation
Right-moving asteroids stay on stack; left-moving collide with top until stable.
// LC 735 - Asteroid Collision
// IDEA: Stack — simulate collisions between right (+) and left (-) asteroids
// time = O(N), space = O(N)
public int[] asteroidCollision(int[] asteroids) {
Deque<Integer> stack = new ArrayDeque<>();
for (int a : asteroids) {
boolean alive = true;
while (alive && a < 0 && !stack.isEmpty() && stack.peek() > 0) {
if (stack.peek() < -a) { stack.pop(); } // stack top destroyed
else if (stack.peek() == -a) { stack.pop(); alive = false; } // both destroyed
else alive = false; // incoming destroyed
}
if (alive) stack.push(a);
}
int[] res = new int[stack.size()];
for (int i = res.length - 1; i >= 0; i--) res[i] = stack.pop();
return res;
}
2-12) Sum of Subarray Ranges (LC 2104) — Dual Monotonic Stack (Contribution Method)
sum(ranges) = sum(subarray maxs) − sum(subarray mins). Use one monotonic stack pass per role; for each popped element compute how many subarrays it owns as the max/min.
Core Idea
range(subarray) = max − min
sum(all ranges) = sum(all subarray maxs) − sum(all subarray mins)
For each element nums[mid], find its left and right dominance boundaries:
- Left boundary
L— index of the previous element that would displacenums[mid]from the max/min role (or-1if none) - Right boundary
R— index of the next element that displaces it (ornif none)
Number of subarrays where nums[mid] is the max/min:
count = (mid − L) × (R − mid)
contribution = nums[mid] × count
The sentinel loop runs i from 0 to n inclusive. When i == n, it flushes every remaining index from the stack using n as the right boundary.
Duplicate-safe boundary rule (avoids double-counting equal elements):
- For max: pop when
nums[mid] < nums[i](strict); left boundary is the last greater-or-equal element. - For min: pop when
nums[mid] > nums[i](strict); left boundary is the last smaller-or-equal element.
Visual Trace — max pass on [1, 3, 2]
Decreasing stack (max contribution)
i=0: push 0 stack=[0]
i=1: nums[0]=1 < nums[1]=3 → pop mid=0
left=-1, right=1
contrib = 1 * (0-(-1)) * (1-0) = 1*1*1 = 1
push 1 stack=[1]
i=2: nums[1]=3 > nums[2]=2, no pop
push 2 stack=[1,2]
i=3 (sentinel): flush
pop mid=2: left=1, right=3 → 2*(2-1)*(3-2) = 2
pop mid=1: left=-1, right=3 → 3*(1-(-1))*(3-1) = 12
max_sum = 1 + 2 + 12 = 15
min pass (increasing stack) → min_sum = 10
answer = 15 − 10 = 5 ✓
verify: [1]=0,[3]=0,[2]=0,[1,3]=2,[3,2]=1,[1,3,2]=2 → sum = 5
Pattern (Python)
# python
# LC 2104 - Sum of Subarray Ranges
# IDEA: sum(ranges) = sum(subarray maxs) - sum(subarray mins)
# Contribution method via monotonic stack — one pass per role
# time = O(N), space = O(N)
def subArrayRanges(nums):
n = len(nums)
def contribution(is_max):
stack = []
total = 0
for i in range(n + 1): # sentinel: i == n flushes remaining
while stack and (
i == n or
(nums[stack[-1]] < nums[i] if is_max else nums[stack[-1]] > nums[i])
):
mid = stack.pop()
left = stack[-1] if stack else -1 # previous boundary index
right = i # current index = right boundary
total += nums[mid] * (mid - left) * (right - mid)
stack.append(i)
return total
return contribution(True) - contribution(False)
Pattern (Java)
// java
// LC 2104 - Sum of Subarray Ranges
// IDEA: sum(ranges) = sum(subarray maxs) - sum(subarray mins)
// Contribution method: for each element count subarrays where it's max/min
// time = O(N), space = O(N)
public long subArrayRanges(int[] nums) {
return contribution(nums, true) - contribution(nums, false);
}
private long contribution(int[] nums, boolean isMax) {
int n = nums.length;
Deque<Integer> stack = new ArrayDeque<>();
long total = 0;
for (int i = 0; i <= n; i++) { // i == n is the sentinel flush
while (!stack.isEmpty()) {
int mid = stack.peek();
boolean shouldPop = (i == n) ||
(isMax ? nums[mid] < nums[i] : nums[mid] > nums[i]);
if (!shouldPop) break;
stack.pop();
int left = stack.isEmpty() ? -1 : stack.peek(); // prev boundary
int right = i; // next boundary
total += (long) nums[mid] * (mid - left) * (right - mid);
}
stack.push(i);
}
return total;
}
Two-Stack Logic Summary
| Pass | Stack type | Pop condition | Computes |
|---|---|---|---|
| Max pass | Monotonic decreasing | nums[mid] < nums[i] |
Sum of subarray maximums |
| Min pass | Monotonic increasing | nums[mid] > nums[i] |
Sum of subarray minimums |
| Both | Sentinel at i = n |
Always flush | Handles right-edge elements |
Similar Problems
| Problem | LC# | Key Difference |
|---|---|---|
| Sum of Subarray Ranges | 2104 | max_sum − min_sum; two monotonic stack passes |
| Sum of Subarray Minimums | 907 | Min contribution only; single increasing stack pass |
| Maximum Subarray Min-Product | 1856 | Min contribution × subarray sum; prefix sums + stack |
| Sum of Total Strength of Wizards | 2281 | Min × sum of sums; prefix of prefix sums + stack |
| Largest Rectangle in Histogram | 84 | Area = height × width; pop on shorter bar |
| Number of Visible People in Queue | 1944 | Count pops per element as the answer |
2-13) Longest Absolute File Path (LC 388) — Stack Indexed by Nesting Depth Priority 5 of 5 — Must know — expect it in almost every loop
Template 7: depth stack. The stack is not monotonic by value — it is monotonic by depth:
stack[d]always holds the accumulated path length at depthd. Before handling a line at depthd, pop untilstack.size() == d, which discards every sibling branch that just ended.
Key idea
"dir\n\tsub1\n\t\tfile.ext\n\tsub2"
line depth pop until size==depth stack (path lengths, '/' included)
dir 0 [] [4] "dir/"
sub1 1 [4] [4, 9] "dir/sub1/"
file.ext 2 [4, 9] (file → no push, len = 9 + 8 = 17)
sub2 1 pop 9 → [4] [4, 9]
depth= number of leading\t; the name is the rest of the line.- A directory pushes
parentLen + name.length() + 1(the+1is the/separator). - A file (name contains
.) never pushes — it only updates the answer withparentLen + name.length().
// java
// LC 388 - Longest Absolute File Path
// IDEA: Stack indexed by nesting depth — stack.peek() = length of the current
// directory prefix (with trailing '/'); pop until size == depth to leave sibling branches
// time = O(N), space = O(D) // N = input length, D = max depth
public int lengthLongestPath(String input) {
Deque<Integer> stack = new ArrayDeque<>(); // prefix length per depth
int maxLen = 0;
for (String line : input.split("\n")) {
int depth = line.lastIndexOf('\t') + 1; // tabs are leading & contiguous
String name = line.substring(depth);
while (stack.size() > depth) stack.pop(); // leave finished branches
int parentLen = stack.isEmpty() ? 0 : stack.peek();
int curLen = parentLen + name.length();
if (name.indexOf('.') >= 0) {
maxLen = Math.max(maxLen, curLen); // file → candidate answer
} else {
stack.push(curLen + 1); // dir → +1 for '/'
}
}
return maxLen;
}
# python
# LC 388 - Longest Absolute File Path
# IDEA: stack[d] = length of the directory prefix at depth d (trailing '/' counted);
# pop until len(stack) == depth so sibling branches are discarded
# time = O(N), space = O(D)
def lengthLongestPath(input: str) -> int:
stack = [] # prefix length per depth
best = 0
for line in input.split('\n'):
depth = len(line) - len(line.lstrip('\t'))
name = line[depth:]
while len(stack) > depth:
stack.pop()
parent = stack[-1] if stack else 0
cur = parent + len(name)
if '.' in name:
best = max(best, cur) # file
else:
stack.append(cur + 1) # dir, +1 for '/'
return best
Pitfalls
- The answer is the longest path to a file, so never update the max on a directory.
- Do not compute depth with
line.count('\t')after slicing — depth must come from the leading tabs only. - Empty input / no file → return
0.
2-14) Longest Valid Parentheses (LC 32) — Index Stack with a Base Sentinel Priority 5 of 5 — Must know — expect it in almost every loop
Template 8: stack of indices + sentinel base. Instead of storing characters, store indices, and seed the stack with
-1as “the index just before the current valid block”. After popping on), the new stack top is the last unmatched index, soi - stack.peek()is the length of the valid run ending ati— no extra length bookkeeping needed.
Two cases on )
pop, then:
stack empty → this ')' is unmatched → push i as the NEW base
stack !empty → length = i - stack.top()
Visual trace on s = ")()())"
i=0 ')' pop -1 → empty → push 0 stack=[0] best=0
i=1 '(' push 1 stack=[0,1]
i=2 ')' pop 1 → top=0 → 2-0 = 2 stack=[0] best=2
i=3 '(' push 3 stack=[0,3]
i=4 ')' pop 3 → top=0 → 4-0 = 4 stack=[0] best=4
i=5 ')' pop 0 → empty → push 5 stack=[5] best=4 ✓
// java
// LC 32 - Longest Valid Parentheses
// IDEA: Stack of indices seeded with -1 (base). On ')' pop; if empty this ')' becomes
// the new base, else answer candidate = i - stack.peek()
// time = O(N), space = O(N)
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // base = index before the current valid block
int best = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) stack.push(i); // unmatched ')' → new base
else best = Math.max(best, i - stack.peek());
}
}
return best;
}
# python
# LC 32 - Longest Valid Parentheses
# IDEA: index stack with -1 sentinel; i - stack[-1] = length of valid run ending at i
# time = O(N), space = O(N)
def longestValidParentheses(s: str) -> int:
stack = [-1] # base index
best = 0
for i, c in enumerate(s):
if c == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i) # new base
else:
best = max(best, i - stack[-1])
return best
Pitfalls
- Forgetting the
-1seed breaks every run that starts at index0. - The stack holds indices, never characters — the whole trick is the index arithmetic.
- O(1)-space alternative: two passes (left→right, then right→left) with
open/closecounters, resetting whenclose > open(resp.open > close).
2-15) Maximum Binary Tree (LC 654) — Monotonic Decreasing Stack Builds a Cartesian Tree Priority 4 of 5 — High value — a gap here costs you rounds
Template 9: monotonic stack that builds a tree. The naive “find max, recurse left/right” is O(n²). A decreasing stack builds the same tree in one pass: everything popped by
numis smaller thannumand sits to its left → it becomesnum’s left subtree; the surviving stack top is greater thannum→numbecomes its right child. Root = bottom of the stack.
nums = [3,2,1,6,0,5]
3 → stack[3]
2 → 3>2, 3.right = 2 stack[3,2]
1 → 2>1, 2.right = 1 stack[3,2,1]
6 → pop 1,2,3 (each becomes 6.left in turn, last popped wins) → stack empty
stack[6] root = 6
0 → 6.right = 0 stack[6,0]
5 → pop 0 → 5.left = 0; top 6 → 6.right = 5 stack[6,5]
// java
// LC 654 - Maximum Binary Tree
// IDEA: Monotonic DECREASING stack of nodes. Nodes popped by num become num's left
// subtree (last popped = direct left child); surviving top adopts num as right child
// time = O(N), space = O(N) // beats the O(N^2) divide & conquer build
public TreeNode constructMaximumBinaryTree(int[] nums) {
Deque<TreeNode> stack = new ArrayDeque<>(); // values decreasing: bottom -> top
for (int num : nums) {
TreeNode cur = new TreeNode(num);
while (!stack.isEmpty() && stack.peek().val < num) {
cur.left = stack.pop(); // last popped ends up as the left child
}
if (!stack.isEmpty()) stack.peek().right = cur;
stack.push(cur);
}
return stack.isEmpty() ? null : stack.peekLast(); // bottom of stack = global max = root
}
# python
# LC 654 - Maximum Binary Tree
# IDEA: monotonic decreasing stack of nodes; popped nodes chain into cur.left,
# remaining top takes cur as its right child; stack[0] is the root
# time = O(N), space = O(N)
def constructMaximumBinaryTree(nums):
stack = [] # node values decreasing
for num in nums:
cur = TreeNode(num)
while stack and stack[-1].val < num:
cur.left = stack.pop() # overwritten each pop -> keeps the LAST popped
if stack:
stack[-1].right = cur
stack.append(cur)
return stack[0] if stack else None
Why cur.left may be overwritten: each pop re-assigns cur.left, and the popped nodes are already linked to each other (an earlier pop is the previous node’s right child), so after the loop cur.left correctly points at the root of the whole popped block.
Related: LC 1008 (Construct BST from Preorder Traversal) uses the mirror idea — a decreasing stack where a larger value becomes the right child of the last popped node.
2-16) Min Stack (LC 155) — Auxiliary Non-Increasing Stack Priority 4 of 5 — High value — a gap here costs you rounds
Template 10: parallel “min stack”. Keep a second stack whose values are non-increasing; its top is always the minimum of the live elements. This is the design-problem face of the monotonic stack.
// java
// LC 155 - Min Stack
// IDEA: second stack keeps a non-increasing sequence of minima; push a new min when
// val <= current min (the '=' is REQUIRED so duplicates survive matching pops)
// time = O(1) per op, space = O(N)
class MinStack {
private final Deque<Integer> stack = new ArrayDeque<>();
private final Deque<Integer> mins = new ArrayDeque<>(); // non-increasing
public void push(int val) {
stack.push(val);
if (mins.isEmpty() || val <= mins.peek()) mins.push(val);
}
public void pop() {
int v = stack.pop();
if (v == mins.peek()) mins.pop();
}
public int top() { return stack.peek(); }
public int getMin() { return mins.peek(); }
}
# python
# LC 155 - Min Stack
# IDEA: auxiliary non-increasing stack of minima; '<=' on push keeps duplicate minima
# time = O(1) per op, space = O(N)
class MinStack:
def __init__(self):
self.stack = []
self.mins = [] # non-increasing
def push(self, val: int) -> None:
self.stack.append(val)
if not self.mins or val <= self.mins[-1]:
self.mins.append(val)
def pop(self) -> None:
if self.stack.pop() == self.mins[-1]:
self.mins.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.mins[-1]
The classic bug: pushing the new min only when val < mins.peek() (strict). With push(0); push(0); pop(); the single stored 0 is removed and getMin() returns the wrong value. Use <=.
Space-saving variant: store (val, minSoFar) pairs in one stack — same O(1) ops, simpler to state under interview pressure.
2-17) Variations of Existing Templates
| LC # | Problem | Base template | The twist |
|---|---|---|---|
| 1475 | Final Prices With a Special Discount in a Shop | Template 2 (next smaller) | Next smaller or equal — pop on prices[stack[-1]] >= prices[i], and the discount is price - prices[i] rather than the index distance |
| 1019 | Next Greater Node In Linked List | Template 1 (next greater) | Same decreasing stack, but the input is a linked list — walk it once into an array (or push (index, val) while walking) since the answer array needs random access |
| 768 | Max Chunks To Make Sorted II | Template 1 (decreasing pops) | Stack holds chunk maxima, not raw elements; answer = final stack size |
| 769 | Max Chunks To Make Sorted | Template 1 (degenerate) | Values are a permutation of 0..n-1, so a running max replaces the stack: cut a chunk whenever runningMax == i |
| 1047 / 1209 | Remove All Adjacent Duplicates In String (I / II) | Template 5 (stack with info) | Stack stores (char, count) pairs; pop when count reaches k — LC 1047 is the k = 2 special case |
Max Chunks To Make Sorted II (LC 768) — chunk-maxima stack
// java
// LC 768 - Max Chunks To Make Sorted II
// IDEA: monotonic increasing stack of chunk MAXIMA. A value smaller than the top must
// merge every chunk it is smaller than; the merged chunk keeps the largest max
// time = O(N), space = O(N)
public int maxChunksToSorted(int[] arr) {
Deque<Integer> stack = new ArrayDeque<>(); // chunk maxima, increasing bottom -> top
for (int num : arr) {
if (!stack.isEmpty() && num < stack.peek()) {
int maxOfMerged = stack.pop();
while (!stack.isEmpty() && num < stack.peek()) stack.pop();
stack.push(maxOfMerged); // merged chunk keeps the old max
} else {
stack.push(num); // starts a new chunk
}
}
return stack.size();
}
# python
# LC 768 - Max Chunks To Make Sorted II
# IDEA: increasing stack of chunk maxima; merging keeps the largest max
# time = O(N), space = O(N)
def maxChunksToSorted(arr):
stack = [] # chunk maxima, increasing
for num in arr:
if stack and num < stack[-1]:
merged_max = stack.pop()
while stack and num < stack[-1]:
stack.pop()
stack.append(merged_max)
else:
stack.append(num)
return len(stack)
# LC 769 - Max Chunks To Make Sorted (values are a permutation of 0..n-1)
# time = O(N), space = O(1)
def maxChunksToSortedI(arr):
chunks, running_max = 0, -1
for i, num in enumerate(arr):
running_max = max(running_max, num)
if running_max == i: # prefix holds exactly the values 0..i
chunks += 1
return chunks
2-18) Steps to Make Array Non-decreasing (LC 2289) — Monotonic Stack Carrying a DP Value Priority 4 of 5 — High value — a gap here costs you rounds
leetcode_python/Stack/steps-to-make-array-non-decreasing.py
Template 11: the stack carries a dp value, not just a position. Every template above computes a final number at the moment of the pop (a width, an area, a distance). Here the popped element hands its dp value up to the element that popped it, so the answers chain:
dp[i] = max(dp of everything I popped) + 1. The answer ismax(dp), the longest such chain.
Core Idea
One round deletes every nums[i] with nums[i-1] > nums[i], all at once. Simulating the rounds is O(n²) (n up to 1e5), so re-ask the question per element:
dp[i] = the round in which nums[i] is deleted (0 = never deleted)
answer = max(dp)
nums[i] is eventually deleted by the nearest element on its left that is strictly greater — but that killer cannot reach nums[i] until everything between them is gone. That waiting time is exactly what the stack hands over:
keep a DECREASING stack of indices, then for each i:
while nums[i] >= nums[stack[-1]]: # this top can never kill me
cur = max(cur, dp[stack.pop()]) # -> inherit its deadline
if stack: dp[i] = cur + 1 # top is > nums[i]: my killer.
# it reaches me one round after
# the last of the popped ones died
else: dp[i] = 0 # nothing bigger on the left -> I survive
Two things the “eating” metaphor makes obvious:
- What gets popped is what I outlive. Anything
<= nums[i]is doomed no later thannums[i]is, so its deadline is a lower bound on mine. +1, not+ popped count. All deletions in a round happen simultaneously, so a whole block of popped elements can vanish in the same round; only the slowest one (max, notsum) delays my killer, and then by exactly one round.
Reading the Stack: What steps_to_remove Means
The stack is easiest to hold in your head as value/deadline pairs rather than indices:
stack = [ [val, steps_to_remove], ... ]
val = the element's value
steps_to_remove = WHICH ROUND this element is deleted in (0 = never)
That second number is not a count of work, not a distance, not a nesting depth — it is a round number on a calendar. Read the ladder off it:
steps_to_remove |
Meaning | Why |
|---|---|---|
0 |
never deleted | nothing strictly greater sits to its left (it is a prefix maximum), so no element can ever delete it |
1 |
deleted in round 1 | its left neighbour is already strictly greater — the rule fires immediately, nobody has to clear the way |
2 |
deleted in round 2 | the block between it and its killer needs 1 round to disappear; it is exposed at the start of round 2 |
3 |
deleted in round 3 | that block needs 2 rounds; only then does the killer become its left neighbour |
k |
deleted in round k |
the block in front of it takes k - 1 rounds to clear, and it dies in the next one |
So steps_to_remove = k always means “k - 1 rounds ahead of me, then my turn” — which is exactly why the recurrence is max(popped) + 1 and why the answer is max over all the deadlines: the array is non-decreasing the round after the last scheduled deletion happens.
One consequence worth stating, because it is the usual off-by-one: an element whose deadline is 0 is not an element deleted in round 0. It is an element with no deadline at all, which is why the code assigns 0 in the stack is empty branch and never feeds it into res.
Visual Trace — nums = [5,3,4,4,7]
i nums[i] pops (dp inherited) stack after dp[i] res
0 5 - [0] 0 (nothing bigger) 0
1 3 none (3 < 5) [0,1] 0+1 = 1 1
2 4 pop 1 -> cur=max(0,dp1)=1 [0,2] 1+1 = 2 2
3 4 pop 2 -> cur=max(0,dp2)=2 [0,3] 2+1 = 3 3
4 7 pop 3 (cur=3), pop 0 [4] 0 (stack emptied) 3
dp = [0,1,2,3,0] -> answer 3
check: round1 deletes 3 -> [5,4,4,7]; round2 deletes the first 4 -> [5,4,7];
round3 deletes the second 4 -> [5,7] ✓
Dry Run — the Full LC Example, (val, steps) Pair Stack
nums = [5,3,4,4,7,3,6,11,8,5,11], expected answer 3. Same algorithm, written with pairs so the deadline is visible on the stack itself. Pop while stack[-1][0] <= val, inherit max(popped steps), then +1 if something is left (else 0):
| # | val |
stack before | popped | steps |
stack after | res |
|---|---|---|---|---|---|---|
| 0 | 5 |
[] |
— | 0 — stack empty, prefix max, never dies |
[(5,0)] |
0 |
| 1 | 3 |
[(5,0)] |
— | 0 + 1 = 1 — 5 > 3 already, dies next round |
[(5,0),(3,1)] |
1 |
| 2 | 4 |
[(5,0),(3,1)] |
(3,1) |
1 + 1 = 2 — must outlive the 3, then 5 takes it |
[(5,0),(4,2)] |
2 |
| 3 | 4 |
[(5,0),(4,2)] |
(4,2) |
2 + 1 = 3 — popped on <=; inherits deadline 2 |
[(5,0),(4,3)] |
3 |
| 4 | 7 |
[(5,0),(4,3)] |
(4,3), (5,0) |
0 — stack emptied, new prefix max |
[(7,0)] |
3 |
| 5 | 3 |
[(7,0)] |
— | 0 + 1 = 1 |
[(7,0),(3,1)] |
3 |
| 6 | 6 |
[(7,0),(3,1)] |
(3,1) |
1 + 1 = 2 |
[(7,0),(6,2)] |
3 |
| 7 | 11 |
[(7,0),(6,2)] |
(6,2), (7,0) |
0 — emptied again |
[(11,0)] |
3 |
| 8 | 8 |
[(11,0)] |
— | 0 + 1 = 1 |
[(11,0),(8,1)] |
3 |
| 9 | 5 |
[(11,0),(8,1)] |
— | 0 + 1 = 1 — 8 > 5 already; the 8 is irrelevant to the 5’s deadline |
[(11,0),(8,1),(5,1)] |
3 |
| 10 | 11 |
[(11,0),(8,1),(5,1)] |
(5,1), (8,1), (11,0) |
0 |
[(11,0)] |
3 |
Result max(steps) = 3.
Now read the deadlines back as a schedule and compare with the rounds the problem statement prints:
idx 0 1 2 3 4 5 6 7 8 9 10
nums 5 3 4 4 7 3 6 11 8 5 11
steps 0 1 2 3 0 1 2 0 1 1 0
<- every nonzero entry is a scheduled funeral
round 1 deletes steps==1 -> idx 1,5,8,9 (3, 3, 8, 5)
[5,3,4,4,7,3,6,11,8,5,11] -> [5,4,4,7,6,11,11]
round 2 deletes steps==2 -> idx 2,6 (the first 4, and 6)
[5,4,4,7,6,11,11] -> [5,4,7,11,11]
round 3 deletes steps==3 -> idx 3 (the second 4)
[5,4,7,11,11] -> [5,7,11,11] non-decreasing, stop
3 rounds == max(steps) ✓
Two rows are worth pausing on, because they are where the intuition usually breaks:
- Row 3 (the second
4). It is popped on<=, not<. Its own deadline is not 1 even though a5sits to its left, because the first4is standing in the way until round 2 — hence3. This is the row a strict>pop gets wrong, and the row that produces the whole answer. - Row 9 (the
5after the8). Four elements are alive on the stack, yet the5dies in round 1: its immediate left neighbour8is already greater, so nothing has to be cleared first. A deep stack does not imply a late deadline — the deadline only depends on what stands between an element and its killer.
And rows 4, 7, 10 show the other half of the invariant: whenever the stack empties, the new element is a prefix maximum, gets deadline 0, and every deadline behind it has already been banked into res — the array in effect restarts from that element.
The >= vs > trap
The deletion rule fires only on nums[i-1] > nums[i], so an equal element is never deleted — which means it is not a killer either, and must be popped like the smaller ones. Writing the pop as strict > is the bug this problem is built to catch:
nums = [5,3,4,4,7]
pop on >= -> dp = [0,1,2,3,0] -> 3 ✓
pop on > -> dp = [0,1,2,1,0] -> 2 ✗ (idx 3 wrongly adopts idx 2 as its killer)
Direction flips the operator, and this is the part worth memorising:
| Scan | Pop condition | What the pops mean |
|---|---|---|
| Left → right | nums[i] >= nums[stack[-1]] (non-strict) |
things that cannot kill me — smaller and equal |
| Right → left | nums[i] > nums[stack[-1]] (strict) |
things that I eat — strictly smaller only |
Pattern (Python)
# python
# LC 2289 - Steps to Make Array Non-decreasing
# IDEA: monotonic DECREASING stack of indices; dp[i] = round in which nums[i] dies.
# A popped element hands its dp up: dp[i] = max(popped dp) + 1
# time = O(n), space = O(n)
def totalSteps(nums):
n = len(nums)
dp = [0] * n # dp[i] = round nums[i] is removed (0 = never)
stack = [] # indices, values monotonically DECREASING
res = 0
for i in range(n):
cur = 0
# NOTE !!! `>=` — an equal element is never deleted, so it is not a killer
while stack and nums[i] >= nums[stack[-1]]:
cur = max(cur, dp[stack.pop()])
if stack: # a strictly greater element on the left = my killer
dp[i] = cur + 1
res = max(res, dp[i])
stack.append(i) # else dp[i] stays 0: never removed
return res
Same dp scanned right → left — each element computes its answer from what it eats, so the “is there a bigger element on my left?” branch disappears:
# python
# LC 2289 - variant: right -> left, steps = max(steps + 1, dp[j])
# time = O(n), space = O(n)
def totalSteps_rtl(nums):
n = len(nums)
dp = [0] * n
stack, res = [], 0
for i in range(n - 1, -1, -1):
steps = 0
while stack and nums[i] > nums[stack[-1]]: # strict: only what I eat
# one more round than eaten so far, but j may itself be dying until dp[j]
steps = max(steps + 1, dp[stack.pop()])
dp[i] = steps
res = max(res, steps)
stack.append(i)
return res
Pattern (Java)
// java
// LC 2289 - Steps to Make Array Non-decreasing
// IDEA: decreasing stack of indices; dp[i] = round nums[i] is removed.
// dp[i] = max(dp of popped) + 1 when a strictly greater element remains on the left
// time = O(N), space = O(N)
public int totalSteps(int[] nums) {
int n = nums.length, res = 0;
int[] dp = new int[n]; // dp[i] = round nums[i] dies (0 = never)
Deque<Integer> stack = new ArrayDeque<>(); // indices, values decreasing
for (int i = 0; i < n; i++) {
int cur = 0;
while (!stack.isEmpty() && nums[i] >= nums[stack.peek()]) {
cur = Math.max(cur, dp[stack.pop()]); // inherit the deadline
}
if (!stack.isEmpty()) {
dp[i] = cur + 1;
res = Math.max(res, dp[i]);
}
stack.push(i);
}
return res;
}
A (value, steps) pair stack works identically and drops the dp array — push (num, cur) instead of the index, pop on stack[-1][0] <= num; that is the form traced above. Use it when the indices are not needed for anything else.
Similar Problems
| Problem | LC # | Key Difference |
|---|---|---|
| Steps to Make Array Non-decreasing | 2289 | Popped element’s dp is inherited: dp[i] = max(popped dp) + 1 |
| Online Stock Span | 901 | Same “carry a value through the pops”, but the aggregate is a sum of popped spans, not a max+1 |
| Car Fleet II | 1776 | Right-to-left stack where each car’s collision time is derived from the popped cars — the same chaining, with a real-valued dp (§2-10) |
| Car Fleet | 853 | A count of survivors, not when anyone dies — so its stack never pops and two variables suffice (§2-10) |
| Count Robot Groups | 4045 | The same greedy as 853 with no finish line, so there is no arrival time to compare — speeds instead (§2-10) |
| Asteroid Collision | 735 | Same “stronger element eats weaker” simulation; the answer is the survivors, so no dp is carried |
| Minimum Cost Tree From Leaf Values | 1130 | Pop while smaller and aggregate a cost at each pop instead of a round number |
| Largest Rectangle in Histogram | 84 | Baseline contrast: the pop computes a final value (area) that nothing inherits |
2-19) Classic Stack Problems Worth Knowing (non-monotonic)
These use a plain stack (no monotonic invariant) but show up constantly alongside the patterns above.
| Problem | LC # | Key Technique | Difficulty |
|---|---|---|---|
| Simplify Path | 71 | Split on /; push components, .. pops, ./empty skipped |
Medium |
| Backspace String Compare | 844 | Stack per string, or two pointers from the back for O(1) space | Easy |
| Remove All Adjacent Duplicates In String | 1047 | Push char, pop when equal to top | Easy |
| Remove All Adjacent Duplicates in String II | 1209 | Stack of (char, count), pop when count hits k |
Medium |
| Flatten Nested List Iterator | 341 | Stack of iterators/lists; flatten lazily in hasNext() |
Medium |
| Binary Search Tree Iterator | 173 | Controlled iterative inorder — stack of left spine | Medium |
| Maximum Frequency Stack | 895 | freq map + map from frequency → stack of values |
Hard |
| Baseball Game | 682 | Straight stack simulation of +, D, C |
Easy |