Jc-alt logo
jc
LeetCode: Stack

LeetCode: Stack

··
65 min read
·data structures and algorithms

Stack Intro

LeetCode problems with elegant solutions using stacks.

Stack Application: Tracking Nested or Hierarchical Structures

We can track structure while iterating over an object ensuring it maintains some criteria

Ex: Validate if a string containing brackets ()[] is properly balanced:

    def balancedParentheses(s: str) -> bool:
        stack = []
        pairs = {')': '(', ']': '[', '}': '{'}
        for char in s:
            if char in pairs.values():
                stack.append(char)
            elif char in pairs:
                if not stack or stack.pop() != pairs[char]:
                    return False
        return not stack

Stack Application: Backtracking by Tracking History or State

We can use stacks in backtracking to store the state of exploration. When a branch reaches a dead end or a solution, we pop the state to return to the previous state and continue exploring other branches.

Ex: Subset Sum with Backtracking

    def subset_sum(nums, target):
        stack = [(0, [], 0)]  # (index, current_subset, current_sum)
        result = []
        
        while stack:
            index, current_subset, current_sum = stack.pop()
            
            if current_sum > target:  # Prune invalid paths
                continue
            
            if current_sum == target:  # Valid solution
                result.append(list(current_subset))
                continue
            
            # Push new states for further exploration
            for i in range(index, len(nums)):
                stack.append((i + 1, current_subset + [nums[i]], current_sum + nums[i]))
        
        return result

    # subset_sum([2, 3, 6, 7], 7) = [[7]]

Stack Application: Monotonic Property Maintenance

A stack can maintain a monotonic property (increasing or decreasing) over a sequence while processing elements, ensuring efficient lookups or modifications.

Ex: Find the Next Greater Element

    def nextGreaterElement(nums):
        stack = []  # Stores indices of elements in decreasing order
        result = [-1] * len(nums)  # Initialize result with -1
        
        for i in range(len(nums)):
            while stack and nums[i] > nums[stack[-1]]:
                idx = stack.pop()
                result[idx] = nums[i]  # Found the next greater element
            stack.append(i)
        
        return result

    # Example: nextGreaterElement([2, 1, 2, 4, 3]) -> [4, 2, 4, -1, -1]

Stack Application: Simulating Recursion or Call Stacks

We can use a stack to emulate recursion by explicitly managing the call stack.

Ex: Traverse a binary tree in preorder (root -> left -> right):

    def preorderTraversal(root):
        if not root:
            return []
        
        stack = [root]  # Start with the root node
        result = []
        
        while stack:
            node = stack.pop()  # Simulate recursion by processing the top of the stack
            if node:
                result.append(node.val)  # Visit the node
                # Push right child first so the left child is processed next
                if node.right:
                    stack.append(node.right)
                if node.left:
                    stack.append(node.left)
        
        return result

    # Example: For a tree with root → 1, left → 2, right → 3, preorderTraversal(root) -> [1, 2, 3]

Stack Application: Expression Evaluation and Parsing

We can use a stack to evaluate or parse expressions by storing operands and incrementally applying operators. This approach is well-suited for postfix and prefix notations.

Ex: Post and Prefix

    def evaluatePostfix(expression):
        stack = []  # To hold operands during evaluation
        
        for token in expression.split():
            if token.isdigit():  # If it's an operand, push it to the stack
                stack.append(int(token))
            else:  # If it's an operator, pop two operands and apply the operator
                b = stack.pop()
                a = stack.pop()
                if token == '+':
                    stack.append(a + b)
                elif token == '-':
                    stack.append(a - b)
                elif token == '*':
                    stack.append(a * b)
                elif token == '/':  # Assuming integer division
                    stack.append(a // b)
        
        return stack.pop()  # Final result is the only item left in the stack

    # Example:
    # Input: "3 4 + 2 * 1 +"
    # Output: 15 (Equivalent to (3 + 4) * 2 + 1)

Stack Application: Dynamic Programming State Compression

We can use a stack to compress the necessary state while scanning through data, especially when enforcing a specific constraint or invariant like monotonicity. Instead of storing the entire history, we prune irrelevant elements from the stack to keep only the most useful summary of the past

Ex: Given an array, partition it into the minimum number of strictly increasing subsequences

    def min_partitions(nums):
        stacks = []  # Each element represents the last number in a subsequence
        
        for num in nums:
            placed = False
            for i in range(len(stacks)):
                # If we can append to subsequence i
                if stacks[i] < num:
                    stacks[i] = num
                    placed = True
                    break
            if not placed:
                # Start a new subsequence (partition)
                stacks.append(num)
        return len(stacks)

    # Example usage:
    nums = [1, 3, 2, 4, 6, 5]
    print(min_partitions(nums))  # Output: 2

Stack Application: Interval and Range Processing

We can use stacks to efficiently process intervals or ranges, such as merging overlapping intervals, calculating spans, or finding next/previous smaller or larger elements within a range.

Ex: Largest Rectangle in Histogram

    def largestRectangleArea(heights):
        stack = []  # stores indices of bars
        max_area = 0
        
        for i, h in enumerate(heights + [0]):  # Add sentinel to flush stack
            while stack and heights[stack[-1]] > h:
                height = heights[stack.pop()]
                left = stack[-1] if stack else -1
                width = i - left - 1
                max_area = max(max_area, height * width)
            stack.append(i)
        
        return max_area

    # Example:
    # Input: [2, 1, 5, 6, 2, 3]
    # Output: 10  (largest rectangle is formed by heights 5 and 6)

20. Valid Parentheses ::2:: - Easy

Topics: String, Stack

Intro

Given a string s containing: ( ) [ ] { }, determine if the input string is valid. An input string is valid if: 1. Open brackets must be closed by the same type of brackets 2. Open brackets must be closed in the correct order. 3. Every close bracket has a corresponding open bracket of the same type.

InputOutput
"()"true
"()"false
"(]"true
"([])"true

Constraints:

1 ≤ s.length ≤ 104

s consists of parentheses only '()[]'

Abstraction

Given a string of brackets, determine whether every open bracket is closed by the same type, in the correct order.

(Nested: a valid string is brackets wrapped inside brackets, so a closer only ever needs to match the innermost open bracket that is still unclosed)

(Stack: the most recent open bracket must be the first one closed, last in first out, so a stack mirrors the nesting)

(Push the open) stack.append(c) = "remember this open bracket until its closer shows up"

(Match the close) mapping[c] == stack.pop() = "does this closer match the innermost unclosed open bracket?"

(Check for empty) not stack = "is there an open bracket to match this closer? at the end, is anything left unmatched?"

Pseudocode

Sol 1: Manual Condition Stack Check
1. stack = []
2. mapping = {closeChar: openChar}
3. for each c in s:
   a. if c is closing:
        if stack empty: return False
        topElem = stack.pop()
        if mapping[c] != topElem: return False
   b. if c is opening:
        stack.append(c)
4. return stack is empty

Solution 1: [Stack] Manual Condition Stack Check [TC Opt] - Stack/Tracking Nested or Hierarchical Structures

    def isValid(self, s: str) -> bool:

        # sc: O(n)
        stack = []

        # Open close paren matching map
        # sc: O(1)
        mapping = {
            ')' : '(',
            ']' : '[',
            '}' : '{'
        }

        # tc: O(n)
        for c in s:

            # Found Closed: 
            # need to match with open
            # tc: O(1)
            if c in ')]}':

                # Empty Check:
                # there is no open paren to match the current closed paren, invalid stack
                # tc: O(1)
                if not stack:
                    return False

                # Check:
                # the closed type should match the paren currently at the top of stack
                # tc: O(1)
                topElem = stack.pop()
                if mapping[c] != topElem:
                    return False

            # Matched open/closed:
            # we have found an open and closed paren,
            # append the open for the next pair for future matching
            # tc: O(1)
            if c in '([{':
                stack.append(c)
                               
        # Finished iterating over string,
        # validate that stack is empty and all paren have been matched
        isStackEmpty = not stack

        # overall: tc O(n)
        # overall: sc O(n)
        return isStackEmpty

150. Evaluate Reverse Polish Notation ::1:: - Medium

Topics: Array, Stack, Math, Design

Intro

You are given an array of strings tokens that represents an arithmetic expression in a Reverse Polish Notation. Evaluate the expression. Return an integer that represents the value of the expression. Note that: The valid operators are '+', '-', '*', and '/'. Each operand may be an integer or another expression. The division between two integers always truncates toward zero. There will not be any division by zero. The input represents a valid arithmetic expression in a reverse polish notation. The answer and all the intermediate calculations can be represented in a 32-bit integer.

InputOutput
["2","1","+","3","*"]9
["4","13","5","/","+"]6
["10","6","9","3","+","-11","","/","","17","+","5","+"]22

Constraints:

1 ≤ tokens.length ≤ 104

tokens[i] is either an operator: "+", "-", "*", or "/", or an integer in the range [-200, 200].

Abstraction

Given an array of tokens in postfix order, evaluate the arithmetic expression it represents.

(Postfix: an operator always comes after its two operands, so by the time we reach an operator its operands are already the two most recent results)

(Stack: the two most recent unused numbers are always the next ones consumed, last in first out, so a stack holds the pending operands)

(Push the number) stack.append(int(token)) = "remember this operand until an operator needs it"

(Pop the operands) b = stack.pop(), a = stack.pop() = "what are the two most recent operands? b comes off first since it was pushed last, so a op b keeps the original order"

(Push the result) stack.append(a op b) = "the computed result is now an operand for a later operator"

(Truncate toward zero) int(a / b) = "divide as a float, then truncate toward zero, since // would floor negatives the wrong way"

(Read the answer) stack[0] = "once every token is consumed, what is the one value left?"

Pseudocode

Sol 1: Stack Postfix Expression Evaluation Algorithm
1. stack = []
2. for each token in tokens:
   a. if token is number:
        stack.append(int(token))
   b. else (operator):
        b = stack.pop()
        a = stack.pop()
        stack.append(a OP b)
3. return stack.pop()

Solution 1: [Stack] Stack Postfix Expression Evaluation Algorithm [TC Opt] - Stack/Expression Evaluation and Parsing

    def evalRPN(self, tokens: List[str]) -> int:
        
        # Reverse Polish Notation:
        # We only push numbers to the stack,
        # and when we hit an operator we pop b the a
        # since the numbers are on the stack in reverse

        # input:    ["4","13","5","/","+"]
        # expected: 4 + (13 / 5) = 6
        # "4"    -> [4]         push() 4 to stack
        # "13"   -> [4, 13]     push() 13 to stack
        # "5"    -> [4, 13, 5]  push() 5 to stack
        # "/"    -> [4]         hit operator, pop() b = 5, pop() a = 13, complete operation int(13 / 5) = 2
        # "2"    -> [4, 2]      push() 2 to stack
        # "+"    -> []          hit operator, pop() b = 2, pop() a = 4, finish operator (4 + 2) = 6
        # "6"    -> [6]         push() 6 to stack

        # [6] stack holds answer

        # sc: stack holds up to n/2 intermediate results O(n)
        stack = []

        # tc: O(n)
        for token in tokens:

            # Found Integer:
            # Cast to int and push() to stack
            if token not in "+-*/":
                stack.append(int(token))
            
            # Found Operation:
            # Pop() 2 numbers from stack, b then a, and compute
            else:

                # tc: pop operation constant O(1)
                b = stack.pop()
                a = stack.pop()

                # Complete Operation: 
                # Push() result to stack
                match token:
                    case "+":
                        stack.append(a + b)
                    case "-":
                        stack.append(a - b)
                    case "*":
                        stack.append(a * b)
                    case "/":
                        
                        # a / b
                        # 13 / 5

                        # Explicit truncation towards zero
                        # -7 / 3         # -2.333  division:        Remainder                                         x
                        # -7 // 3        # -3      floor division:  Rounds down "towards infinity"                    x
                        # int(-7 / 3)    # -2      int(division):   Rounds up "towards 0", as required by RPN      this one

                        stack.append(int(a / b))

        # Top of stack holds result
        res = stack[0]

        # overall: tc O(n)
        # overall: sc O(n)
        return res

735. Asteroid Collision ::1:: - Medium

Topics: Array, Stack, Simulation

Intro

We are given an array asteroids of integers representing asteroids in a row. The indices of the asteroid in the array represent their relative position in space. For each asteroid, the absolute value represents its size, and the sign represents its direction (positive meaning right, negative meaning left). Each asteroid moves at the same speed. Find out the state of the asteroids after all collisions. If two asteroids meet, the smaller one will explode. If both are the same size, both will explode. Two asteroids moving in the same direction will never meet.

InputOutput
asteroids = [5,10,-5][2,-1,2]
nums = [1,2,3,4,3][2,3,4,-1,4]

Constraints:

1 ≤ nums.length ≤ 10^4

10^9 ≤ nums[i] ≤ 10^9

Abstraction

Given an array of asteroids where sign is direction and absolute value is size, return the asteroids that survive after all collisions.

(Collision: only a right mover with a left mover coming at it from behind can meet, so a collision is only possible when a new left mover arrives and the most recent survivor is a right mover)

(Stack: the new asteroid can only hit the most recent surviving asteroid first, last in first out, so a stack holds the survivors in order)

(Monotonic: a left mover keeps destroying smaller right movers on top of the stack until it hits a bigger one or the top is no longer a right mover, so the stack never holds a right mover directly below a left mover)

(Check for a collision) curr < 0 and stack and 0 < stack[-1] = "is a left mover about to hit the right mover on top of the stack?"

(Compare sizes) stack[-1] < -curr = "which one is bigger? the smaller explodes, equal sizes both explode"

(Track if curr survives) alive = "has curr been destroyed? if so, it must not be pushed"

(Push the survivor) stack.append(curr) = "no collision or curr won every collision, so it is now the most recent survivor"

(Read the answer) stack = "once every asteroid is processed, which ones are still alive, in order?"

So two right movers (+ +) will never meet. And two left movers (- -) will never meet. A right mover in front of a left mover (- +) will never meet. Only a front mover behind a left mover (+ -) will meet.

Pseudocode

Sol 1: Collision Resolution With Alive Flag

1. self.stack = []

2. for curr in asteroids:
    a. alive = True
    b. while alive and curr < 0 and stack and 0 < stack[-1]:
             if stack[-1] < -curr:
                 stack.pop()
                 continue
             elif -curr < stack[-1]:
                 alive = False
             else:
                 stack.pop()
                 alive = False
             if alive:
    c. stack.append(curr)

3. survivors = stack

3. return survivors

Solution 1: [Monotonic Stack] Collision Resolution With Alive Flag - Stack/Simulation

    def asteroidCollision(self, asteroids: List[int]) -> List[int]:
        
        # Collision Rule:
        # - Two stones only hit if a right mover behind a left mover (+ -)
        # - Same direction asteroids never meet (they move at same speed) (+ +) or (- -)
        # - A left mover behind a right mover will never hit (- +).

        # Tracking Survivors:
        # - Everything else gets pushed to stack to track surviving asteroids
        # - Only collide if a right mover hits a left mover (+ -)
        # - so we only collide if we find a new left mover 
        #   while the top of the stack holds a right mover

        # Stack:
        # - stores both left and right movers
        # - uses tracking survivors + collision rules to determine what to pop/push

        # Stack Progression Example: [-3, 10, -5]
        # - (-3) survives (no collisions)
        # - 10 survives (no collisions)
        # - (-5) destroyed (collides with 10)
        # - stack = [-3, 10]

        # sc: O(n)
        stack = []

        # tc: O(n)
        for curr in asteroids:

            # Alive:
            # - is curr alive
            alive = True

            # Right Movers:
            # - pushed immediately to the stack
            # - can never collide with anything already on the stack

            # Left Movers:
            # - if stack is empty, push to stack
            # - if top of stack is also a left mover, push to stack (can't hit reach each other)
            # - check if top stack is a right mover (collision will occur)
            # - if beats top of stack, continue to compare to top of stack

            # Collision If:
            # - curr is alive left mover (alive & negative: curr < 0)
            # - top of stack is right mover, (positive: 0 < stack[-1])
            # - then collide, either left or right survives or none
            # tc: O(n)
            while alive and curr < 0 and stack and 0 < stack[-1]:

                # Left mover is bigger than right:
                # - pop the destroyed right 
                # - keep compare curr against new top
                if stack[-1] < -curr:
                    stack.pop()
                    continue

                # Right mover is bigger than left:
                # - alive flag to false for destroyed left
                # - iterate to next curr
                elif -curr < stack[-1]:
                    alive = False


                # Neither survives:
                # - pop the destroyed right
                # - alive flag to false for destroyed left
                else:
                    stack.pop()
                    alive = False

            # Check for survivor: 
            # - no collision, always alive: (- +), (+ +), or (- -)
            # - left mover destroyed all right movers on top of stack
            if alive:
                stack.append(curr)

        # Stack holds any left or right movers that survived
        survivors = stack

        # overall: tc O(n)
        # overall: sc O(n)
        return survivors

496. Next Greater Element I ::1:: - Easy

Topics: Array, Hash Table, Stack, Monotonic Stack

Intro

The next greater element of some element x in an array is the first greater element that is to the right of x in the same array. You are given two distinct 0-indexed integer arrays nums1 and nums2, where nums1 is a subset of nums2. For each 0 lte i lt nums1.length, find the index j such that nums1[i] == nums2[j] and determine the next greater element of nums2[j] in nums2. If there is no next greater element, then the answer for this query is -1. Return an array ans of length nums1.length such that ans[i] is the next greater element as described above. Follow up: Could you find an O(nums1.length + nums2.length) solution?

InputOutput
nums1 = [4,1,2], nums2 = [1,3,4,2][-1,3,-1]
nums1 = [2,4], nums2 = [1,2,3,4][3,-1]

Constraints:

1 ≤ nums.length ≤ nums2.length ≤ 1000

0 ≤ nums1[i], nuns2[i] ≤ 10^4

All integers in nums1 and nums2 are unique.

All the integers of nums1 also appear in nums2.

Abstraction

Given two arrays where nums1 is a subset of nums2, return for each number in nums1 the first greater number to its right in nums2, or -1 if none exists.

(Next greater: each number in nums2 is waiting for the nearest larger number to its right, so we resolve numbers as soon as that larger number shows up)

(Stack: the most recent unresolved number is the first one a new larger number can resolve, last in first out, so a stack holds the numbers still waiting)

(Monotonic: a new number resolves every waiting number smaller than it, so the stack of waiting numbers is always decreasing from bottom to top)

(Hash Map: nums1 asks about values, not positions, and all values are unique, so solve every number in nums2 once and look up each query in O(1))

(Check for a greater element) stack[-1] < curr = "is the current number larger than the number on top of the stack?"

(Record the answer) nextGreater[stack.pop()] = curr = "this waiting number just found its next greater element, so save it by value and remove it from the stack"

(Push the pending number) stack.append(curr) = "the current number now waits for its own next greater element"

(Leftover numbers) stack = "once the loop ends, which numbers never found a greater element? they are missing from the map"

(Answer the queries) nextGreater[query] if query in nextGreater else -1 = "what is the next greater element for this nums1 value, or -1 if it never got resolved?"

Pseudocode

Sol 1: Decreasing Stack With Hash Map Lookup
1. (nextGreater = {})
2. (stack = [])
3. for curr in nums2:
    a. while stack and stack[-1] < curr:
         foundNextForIndex = stack.pop()
         nextGreater[foundNextForIndex] = curr
    b. stack.append(curr)
4. (res = [])
5. for query in nums1:
    a. if query in nextGreater:
         res.append(nextGreater[query])
       else:
         res.append(-1)
6. return res

Solution 1: [Monotonic Stack] Decreasing Stack With Hash Map Lookup - Stack/Monotonic Stack Next Greater Element

    def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]:

        # Monotonic Stack:
        # - scan nums2 left to right
        # - maintain a stack strictly decreasing from bottom to top. 
        
        # [0] ... [i]
        # high     low
        
        # - if curr number is greater than top of stack,
        #   we have found the 'next greater element' for that top of stack
        # - continue comparing curr to top of stack
        # - anything left on top of stack does not have a next greater element

        # Hash Map:
        # - 
        # Since nums1 is a subset of nums2, we only need to solve
        # "next greater element" once per number in nums2, then look up
        # each answer for nums1 in O(1) instead of resolving it twice.

        # Hashmap:
        # - num2 value => next greatest element
        # - constraints guarantee all values in nums2 are unique, so it's
        #   safe to key this hashmap by value directly
        # - if duplicates were allowed, a value-keyed hashmap would break --
        #   a later occurrence of the same value could silently overwrite
        #   an earlier index's already-correct answer (this is exactly
        #   why 503 keys its stack/result by index instead of value)
        # - and we don't want to do by index here,
        #   is because the queries from nums1 are queries by value, not by index
        # sc: O(n)
        nextGreater = {}

        # sc: O(n)
        stack = []

        # Generate the next greatest element hashmap
        # tc: O(n)
        for curr in nums2:

            # Found Next Greatest:
            # - curr is greater than top of stack
            # - pop top of stack and record curr as its next greatest 
            while stack and stack[-1] < curr:

                # Next Greatest:
                # record curr as the next greatest for top of stack
                foundNextForIndex = stack.pop()
                nextGreater[foundNextForIndex] = curr

            # Pending:
            # - push curr to stack, waiting for its next greatest element
            stack.append(curr)

        # Next greatest num index for query stream
        # sc: O(n)
        res = []

        # Use next greatest element hashmap to respond to queries in nums1
        # tc: O(m), m = len(nums1)
        for query in nums1:

            # Query Stream:
            # - check we found a next greatest for num
            # - if not, return -1
            if query in nextGreater:
                res.append(nextGreater[query])
            else:
                res.append(-1)

        # overall: tc O(n + m)
        # overall: sc O(n)
        return res

503. Next Greater Element II ::1:: - Medium

Topics: Array, Stack, Monotonic Stack

Intro

Given a circular integer array nums (i.e., the next element of nums[nums.length - 1] is nums[0]), return the next greater number for every element in nums. The next greater number of a number x is the first greater number to its traversing-order next in the array, which means you could search circularly to find its next greater number. If it doesn't exist, return -1 for this number.

InputOutput
nums = [1,2,1][2,-1,2]
nums = [1,2,3,4,3][2,3,4,-1,4]

Constraints:

1 ≤ nums.length ≤ 10^4

10^9 ≤ nums[i] ≤ 10^9

Abstraction

Given a circular array, return for each number the first greater number found by moving right and wrapping around to the start, or -1 if none exists.

(Next greater: each number is waiting for the nearest larger number to its right, so we resolve numbers as soon as that larger number shows up)

(Circular: the next greater of a number near the end may sit near the start, so scanning the array twice lets every index see every other number after it, using i % n instead of building a doubled array)

(Stack: the most recent unresolved index is the first one a new larger number can resolve, last in first out, so a stack holds the indices still waiting)

(Monotonic: a new number resolves every waiting index whose value is smaller than it, so the stack of waiting values is always decreasing from bottom to top)

(Indices: values can repeat, so a value-keyed map would overwrite answers, store indices on the stack and write each answer into res by index)

(Wrap the index) curr = nums[i % n] = "what number is at this position of the simulated doubled scan?"

(Check for a greater element) nums[stack[-1]] < curr = "is the current number larger than the number at the index on top of the stack?"

(Record the answer) res[stack.pop()] = curr = "this waiting index just found its next greater element, so save it at its own position and remove it from the stack"

(Push only on the first pass) if i < n: stack.append(i) = "each index waits once, the second pass only resolves leftover indices and must not add duplicates"

(Leftover indices) res = [-1] * n = "once both passes end, which indices never found a greater element? they keep -1"

Pseudocode

Sol 1: Decreasing Stack of Indices Simulated Circular Scan
1. (n = len(nums))
2. (res = [-1] * n)
3. (stack = [])
4. for i in range(2 * n):
    a. curr = nums[i % n]
         while stack and nums[stack[-1]] < curr:
             foundNextForIndex = stack.pop()
             res[foundNextForIndex] = curr
    b. if i < n:
         stack.append(i)
5. return res

Solution 1: [Monotonic Stack] Decreasing Stack of Indices Simulated Circular Scan - Stack/Monotonic Stack Next Greater Element

    def nextGreaterElements(self, nums: List[int]) -> List[int]:

        # Monotonic Stack (Circular):
        # - same idea as Next Greater Element I, but array is circular, 
        #   so element at end of list could have next greater near beginning of list
        # - simulate wrapping around by scanning the array twice
        #   (indices 0..2n-1), using i % n to map back into the real
        #   array, without actually building a doubled array in memory

        # [0] ... [i]
        # high     low

        # - if curr number is greater than the number at the index on top of stack, 
        #   we have found the 'next greater element' for that index
        # - continue comparing curr to the new index on top of the stack
        # - anything left on the stack after second passes does not have a next greater

        # Indices Instead Of Values:
        # - unlike Next Greater Element I, nums are not guaranteed unique values
        # - we can't use a value -> answer hashmap due to potential duplicate overwrite
        # - instead we store indexes and write next greatest into res array by index

        n = len(nums)

        # Next greatest num index
        # sc: O(n)
        res = [-1] * n

        # sc: O(n)
        stack = []

        # Simulated circular scan:
        # - pass around array twice to ensure last element
        #   can find its next greatest element if it exists via wrap around
        # tc: O(2n) ~ O(n)
        for i in range(2 * n):

            # Wrap:
            # - Turn doubled range back in bounds of real array
            curr = nums[i % n]

            # Found Next Greatest:
            # - curr is greater than the number at the index on top of stack
            # - pop that index and record curr as its next greatest
            while stack and nums[stack[-1]] < curr:

                # Next Greatest:
                # record curr as the next greatest for top of stack
                foundNextForIndex = stack.pop()
                res[foundNextForIndex] = curr

            # Booking Keeping:
            # - only push one copy of elements
            # - do not push anything during second iteration
            if i < n:
                stack.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return res

739. Daily Temperatures ::3:: - Medium

Topics: Array, Stack, Monotonic Stack, Two Pointers, Dynamic Programming

Intro

Given an array of integers temperatures represents the daily temperatures, return an array answer such that answer[i] is the number of days you have to wait after the ith day to get a warmer temperature. If there is no future day for which this is possible, keep answer[i] == 0 instead.

InputOutput
[30,40,50,60][1,1,1,0]
[30,60,90][1,1,0]
[73,74,75,71,69,72,76,73][1,1,4,2,1,1,0,0]

Constraints:

1 ≤ temperatures.length ≤ 105

30 ≤ temperatures[i] ≤ 100

Abstraction

Given an array of daily temperatures, return for each day the number of days until a warmer temperature, or 0 if none exists.

(Next greater: each day is waiting for the nearest warmer day to its right, so we resolve days as soon as that warmer day shows up, and the answer is the distance between their indices)

(Stack: the most recent unresolved day is the first one a new warmer day can resolve, last in first out, so a stack holds the indices of days still waiting)

(Monotonic: a new day resolves every waiting day colder than it, so the temperatures of the waiting days are always decreasing from bottom to top)

(Indices: the answer is a number of days, not a temperature, so the stack stores indices and each wait time is the difference between two indices)

(Check for a warmer day) temperatures[stack[-1]] < temperatures[i] = "is today warmer than the day on top of the stack?"

(Compute the wait) res[stack.pop()] = i - coldDayIndex = "how many days did this waiting day have to wait for today? save it at its own position and remove it from the stack"

(Push the pending day) stack.append(i) = "today is now the coldest waiting day, still looking for a warmer day"

(Leftover days) res = [0] * n = "once the loop ends, which days never found a warmer day? they keep 0"

Pseudocode

Sol 1: Monotonic Decreasing Stack of Cold Temps
1. (n = len(temperatures))
2. (res = [0] * n)
3. (stack = [])
4. for i in range(n):
    a. while stack and temperatures[stack[-1]] < temperatures[i]:
         hotDayIndex = i
         coldDayIndex = stack.pop()
         coldDayWaitTime = hotDayIndex - coldDayIndex
         res[coldDayIndex] = coldDayWaitTime
    b. stack.append(i)
5. return res


Sol 3: Reverse Iteration With Jump Traversal Using Dynamic Programming
1. (n = len(temperatures))
2. (dp = [0] * n)
3. (maxHottestDay = n-1)
4. for i in range(n-2, -1, -1):
    a. if temperatures[maxHottestDay] <= temperatures[i]:
         maxHottestDay = i
    b. else:
         tempCandidateIndex = i+1
         while temperatures[tempCandidateIndex] <= temperatures[i]:
             tempCandidateIndex = tempCandidateIndex + dp[tempCandidateIndex]
         dp[i] = tempCandidateIndex - i
5. return dp

Solution 1: [Monotonic] Monotonic Decreasing Stack of Cold Temps - Stack/Monotonic Property Maintenance

    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        
        # Monotonic Stack: 
        # - maintains indexes for monotonic decreasing temperatures 
        # - When monotonic decreasing rule breaks, that means we have found a hotter temperature 
        #   for at least 1 of the previous temperatures
        
        # Stack:
        #      *                                   *                             *                        *                       *
        #      *  *                                *  *                          *  *                     *  *                    *  *    
        #      *  *             *                  *  *             *            *  *          *          *  *       *            *  *  * 
        #      *  *  *          *                  *  *  *          *            *  *  *       *          *  *       *            *  *  * 
        #      *  *  *          *                  *  *  *          *            *  *  *       *          *  *       *            *  *  * 
        #      *  *  *  *   +   *                  *  *  *  *   +   *            *  *  *   +   *          *  *   +   *            *  *  * 
        #     ------------     ---       ==>      ------------     ---    ==>   ---------     ---   ==>  ------     ---   ==>   --------- 
        #      0  1  2  3       4      calc wait   0  1  2  3       4            0  1  2       4          0  1       4   join()   0  1  4
        #     older          hot day     days          
        #                                          Day 3 waits 1 day           Day 2 waits 2 days     Day 4 is colder than Day 1

        n = len(temperatures)

        # Hotter day wait time for temperatures 
        # sc: O(n)
        res = [0] * n

        # Stores indexes for temperatures
        # sc: O(n)
        stack = []

        # tc: O(n)
        for i in range(n):

            # Hotter Temperature:
            # - if stack is non empty, verify monotonic decreasing
            # - if monotonic decreasing broken, curr temperature is hotter for at least 1 of the previous temperatures
            # - pop indexes for temperatures off the stack until monotonic decreasing is true again
            # - for each of the indexes for temperatures popped, we can calculate a hot day wait time
            while stack and temperatures[stack[-1]] < temperatures[i]:
                
                # i: Hotter day index
                hotDayIndex = i

                # stack[-1]: Cold day index
                coldDayIndex = stack.pop()

                # Wait time:
                # - Hot day is the closest hotter day for the cold day,
                # - Day 5 to Day 3 has a way time of 2 days, so wait time = (5 - 3) = 2
                coldDayWaitTime = hotDayIndex - coldDayIndex

                # Set wait time for current cold day
                res[coldDayIndex] = coldDayWaitTime

            # Monotonic Decrease:
            # - appending index for temperature will keep monotonic decreasing true
            # - this is the new coldest day on the stack
            # - this is now waiting for a new hotter day
            stack.append(i)  
        
        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 2: [Monotonic] Reverse Iteration Monotonic Decreasing Stack of Warm Temps [SC Opt] - Stack/Monotonic Property Maintenance

    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        
        # Aggressive Pruning:
        # - Reverse tends to use less actual in memory
        # - In reverse, we are store hot temperatures in monotonic decreasing order
        # - This ensures we have a list of the most recent hot temperatures 
        #   to ensure minimum wait time for cold days 
        # - So colder temperatures are immediately resolved for their wait time, 
        #   so colder days are never stored waiting for hotter days 
        #   which can use less memory depending on the data

        # Temperatures:
        # temp:[ 1, 2, 4, 5, 4, 3, 1]
        # index: 0  1  2  3  4  5  6

        # Iterating Backwards + Monotonic Decrease (what we want):
        # - we represent increasing temperatures
        # - by storing hotter temperatures with the more recent days being on top of the stack
        
        #   list of temperatures            our stack
        
        #              *                     *              
        #           *  *                     *  *           
        #           *  *                     *  *           
        #        *  *  *                     *  *  *        
        #     *  *  *  *      represented    *  *  *  *    
        #   ---------------      by =>      --------------- 
        #     0  1  2  3      our stack      3  2  1  0
        

        n = len(temperatures)

        # Hotter day wait time for temperatures 
        # sc: O(n)
        res = [0] * n

        # Stores indexes for temperatures
        # sc: O(n)
        stack = []

        # tc: O(n)
        for i in range(n-1, -1, -1):

            # Hotter Temperature:
            # - if stack is non empty, verify monotonic decreasing
            # - if monotonic decreasing broken, curr temperature can serve as a hotter more recent temperature
            # - pop older hot temperature and replace with newer hotter temperature
            # - ensures minimum wait time for cold days
            while stack and temperatures[stack[-1]] <= temperatures[i]:
                stack.pop()

            # Monotonic Decreasing:
            # - curr is colder than top of stack
            # - ergo, everything on stack is hotter than curr
            # - top of stack guaranteed to be the closest hottest day to curr
            # - calculate wait days for current cold day
            if stack:

                # Closest hotter day to curr
                futureHotDayIndex = stack[-1]

                # Cold day curr
                currentColdDayIndex = i

                # Wait time calculation
                waitDays = futureHotDayIndex - currentColdDayIndex

                # Set wait time for current cold day
                res[i] = waitDays


            # Monotonic Decrease:
            # - appending index for temperature will keep monotonic decreasing true
            # - this is least hottest day on the stack
            # - this is acting as the most recent hottest day
            stack.append(i)        
        
        # overall: tc O(n)
        # overall: sc O(n)
        return res

Solution 3: [Dynamic Programming] Reverse Iteration With Jump Traversal Using Dynamic Programming Building Future Warm Temperatures List [TC Opt] - Stack/Algorithm

    def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
        
        # Dynamic Programming:
        # - Use previously found hottest day to serve as jump skips for curr temperature

        # Jump Traversal:
        # - for curr i, try i+1 as the next hottest day
        # - if i+1 is the next hottest day, this is easy 1 day wait
        # - if its not, then the next hottest day for i+1, can also apply for i
        # - skip to next hottest day for i+1, to serve as a candidate for i

        n = len(temperatures)

        # Hotter day wait time for temperatures 
        # sc: o(n)
        dp = [0] * n

        # Hottest day found so far:
        maxHottestDay = n-1

        # Reverse iteration to build next hottest day dp list
        # tc: O(n)
        for i in range(n-2, -1, -1):

            # New Hottest Day
            # - curr is hotter than max hottest day
            # - no future possible days exist that are hotter
            if temperatures[maxHottestDay] <= temperatures[i]:
                maxHottestDay = i

            # Next Hottest Possible: 
            # - curr has at least 1 hotter day in the future
            # - calculate wait days for curr
            else:

                # Init candidate:
                # - curr's first candidate is the next day
                tempCandidateIndex = i+1

                # Update Candidate: 
                # - if candidate is not a hotter day
                # - jump to the candidates own next hottest
                while temperatures[tempCandidateIndex] <= temperatures[i]:
                    
                    # Jump to candidate's own next hottest day:
                    # candidate's next hottest day = candidate index + wait days time
                    tempCandidateIndex = tempCandidateIndex + dp[tempCandidateIndex]

                # Found next hottest day:
                # - Set wait time for curr
                dp[i] = tempCandidateIndex - i

        # overall: tc O(n)
        # overall: sc O(n)
        return dp

853. Car Fleet ::2:: - Medium

Topics: Array, Stack, Sorting, Monotonic Stack

Intro

There are n cars at given miles away from the starting mile 0, traveling to reach the mile target. You are given two integer array position and speed, both of length n, where position[i] is the starting mile of the ith car and speed[i] is the speed of the ith car in miles per hour. A car cannot pass another car, but it can catch up and then travel next to it at the speed of the slower car. A car fleet is a car or cars driving next to each other. The speed of the car fleet is the minimum speed of any car in the fleet. If a car catches up to a car fleet at the mile target, it will still be considered as part of the car fleet. Return the number of car fleets that will arrive at the destination.

InputOutput
target = 10, position = [3], speed = [3]1
target = 100, position = [0,2,4], speed = [4,2,1]1
target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]3

Constraints:

1 ≤ n ≤ 105

0 < target ≤ 1010

0 ≤ position[i] < target

All of values of position are unique

0 < speed[i] ≤ 106

Abstraction

Given cars with starting positions and speeds heading to a target, where a car cannot pass the one ahead and merges into its fleet when it catches up, return the number of fleets that arrive.

(Sorting: a car can only be blocked by cars ahead of it, so sort by position and process from closest to the target to farthest, so each car only needs to compare against the fleet directly ahead)

(Time to target: a car catches the fleet ahead exactly when its own arrival time is less than or equal to that fleet's arrival time, so compare arrival times instead of simulating the drive)

(Stack: the most recent fleet is the only one the next car can catch up to, last in first out, so a stack holds the arrival time of each fleet)

(Monotonic: a car that arrives sooner than the fleet ahead merges into it and is dropped, so the arrival times on the stack are always increasing from bottom to top, and the stack size is the number of fleets)

(Sort closest first) cars.sort(reverse=True) = "which cars are ahead of this one? process the closest to the target first"

(Compute the arrival time) (target - pos) / spd = "how long would this car take to reach the target if nothing blocked it?"

(Check for a merge) stack and currTimeToTarget < stack[-1] = "does this car arrive sooner than the slowest fleet ahead? if so, it catches up and joins that fleet"

(Start a new fleet) stack.append(currTimeToTarget) = "this car is slower than the fleet ahead, so it never catches up and becomes the new slowest fleet"

(Read the answer) len(stack) = "once every car is processed, how many fleets remain?"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Monotonic] [Sorting] Increasing Stack of (Slower aka Higher) Fleet Times Tracking Monotonic Pattern [TC Opt] - Stack/Monotonic Property Maintenance

    def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
        
        # Fleet Rule:
        # - a car that catches up to a slower car ahead of it is forced to go at the slower
        #   cars speed, which creates a fleet
        # - a car that never catches up to the car ahead of it forms its own fleet

        # Sorting By Distance To Target: 
        # - sort cars by closest to target first and farthest last

        # [0]      ...  [n-1]
        # (closest)    (farthest)
        #  higher val   lower val

        # Sort By Distance Implication:
        # - if a farther car has a lower time to target than the car ahead of it,
        #   it will catch up to that car and join its fleet
        # - that car ahead of it sets the time to target for the entire fleet

        # Iterate from closest cars to farthest cars: 
        #  - Check each car with the car/fleet ahead of it
        #  - If curr car has a lower time to arrival (faster) than the car/fleet ahead of it,
        #    it will join that fleetCompare arrival time with the most recently created fleet
        #  - If car has a higher time to arrival (slower) than the car/fleet ahead of it,
        #    it will be left behind and starts a new fleet

        # Monotonic Increasing Stack:
        # - top of stack holds the fleet with the slowest time to arrival
        # - we only need to check if a farther car catches up with the slowest fleet
        #   or if it becomes its own new slowest fleet


        # Tracks number of fleets from from low -> high time to arrival
        # sc: O(n)
        stack = []

        # Prepare to sort by distance:
        # O(n)
        cars = list(zip(position, speed))

        # Sort by descending (higher/closer to farther/lower):
        # tc: timSort O(n log n)
        cars.sort(reverse=True)

        # Iterate from closer to farther
        # tc: O(n) 
        for (pos, spd) in cars:

            # Time To Target:
            currTimeToTarget = (target - pos) / spd 

            # Monotonic Increasing Broken:
            # - curr car has lower time to target than top of stack (slowest fleet)
            # - curr car catches up to fleet, and now must follow that fleets time to target
            if stack and currTimeToTarget < stack[-1]:

                # Top of stack stays the same,
                # current slowest fleet stays the same
                continue

            # Monotonic Increasing Kept:
            # - stack is empty
            # - curr time to target is greater/slower than top of stack
            # - new curr becomes the new slowest fleet
            else:

                # Curr becomes new slowest fleet
                stack.append(currTimeToTarget)

        # Stack is storing all fleet time to targets,
        # total count of time to targets representing count of fleets
        numOfFleets = len(stack)

        # overall: tc O(n log n)
        # overall: sc O(n)
        return numOfFleets

Solution 2: [Greedy] Track Current Fleet With Greedy Assumption [SC Opt] - Stack/Algorithm

    def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
        
        # Greedy Monotonic Stack Compression:
        # - previous stack is doing 2 things:
        #    tracks slowest fleet at top of stack
        #    tracks number of fleets by number of time to targets on stack
        # - we can do both of these with variables

        # Slowest Time To Target Variable:
        # - give a new car, compare to the slowest time to target
        # - if new car has a higher time to target, it is slower, and it becomes the new slowest
        # - if new car has a lower time to target, it is faster, and catches up to the slowest
        #   with the slowest time to target staying the same

        # Fleet Rule:
        # - a car that catches up to a slower car ahead of it is forced to go at the slower
        #   cars speed, which creates a fleet
        # - a car that never catches up to the car ahead of it forms its own fleet

        # Sorting By Distance To Target: 
        # - sort cars by closest to target first and farthest last

        # [0]      ...  [n-1]
        # (closest)    (farthest)
        #  higher val   lower val

        # Sort By Distance Implication:
        # - if a farther car has a lower time to target than the car ahead of it,
        #   it will catch up to that car and join its fleet
        # - that car ahead of it sets the time to target for the entire fleet

        numFleets = 0

        slowestFleetTimeToTarget = 0

        # Prepare to sort by distance:
        # O(n)
        cars = list(zip(position, speed))

        # Sort by descending (higher/closer to farther/lower):
        # tc: timSort O(n log n)
        cars.sort(reverse=True)

        # Iterate from closer to farther
        # tc: O(n) 
        for (pos, spd) in cars:
            
            # Time To Target:
            currCarTimeToTarget = (target - pos) / spd 

            # Curr Car Slower:
            # - curr car is slower than the current slowest time to target,
            #   so it can't catch up and starts a new fleet
            if slowestFleetTimeToTarget < currCarTimeToTarget: 

                # Update slowest time to target
                slowestFleetTimeToTarget = currCarTimeToTarget

                # Create new fleet
                numFleets += 1

            # Curr Car Faster:
            # - curr car is faster than the current slowest time to target,
            #   so it catches up and joins that fleet
            else:

                # Slowest fleet stays the same,
                # no new fleet created
                continue

        # overall: tc O(n log n)
        # overall: sc O(1)
        return numFleets

1475. Final Prices With a Special Discount in a Shop ::1:: - Easy

Topics: Array, Stack, Monotonic Stack

Intro

You are given an integer array prices where prices[i] is the price of the ith item in a shop. There is a special discount for items in the shop. If you buy the ith item, then you will receive a discount equivalent to prices[j] where j is the minimum index such that j > i and prices[j] lte prices[i]. Otherwise, you will not receive any discount at all. Return an integer array answer where answer[i] is the final price you will pay for the ith item of the shop, considering the special discount.

InputOutput
prices = [8,4,6,2,3][4,2,4,2,3]
prices = [1,2,3,4,5][1,2,3,4,5]
prices = [10,1,1,6][9,0,1,6]

Constraints:

1 ≤ prices.length ≤ 5000

1 ≤ prices[i] ≤ 1000

Abstraction

Given an array of prices, subtract from each price the first later price that is less than or equal to it, and return the final prices.

(Next smaller: each price is waiting for the nearest price to its right that is less than or equal to it, so we resolve prices as soon as that later price shows up)

(Stack: the most recent unresolved price is the first one a new smaller price can discount, last in first out, so a stack holds the indices still waiting)

(Monotonic: a new price resolves every waiting price that is greater than or equal to it, so the stack of waiting prices is always increasing from bottom to top)

(Check for a discount) prices[i] lte prices[stack[-1]] = "is the current price low enough to be the discount for the price on top of the stack?"

(Apply the discount) prices[stack.pop()] -= prices[i] = "this waiting price just found its first later discount, so reduce it and remove it from the stack"

(Push the pending index) stack.append(i) = "the current price now waits for its own discount from a later price"

(Leftover indices) stack = "once the loop ends, which prices never found a discount? they keep their original price"

Pseudocode

Sol 1: Increasing Stack of Pending Indices
1. stack = []
    a. for i in range(len(prices)):
         while stack and prices[i] <= prices[stack[-1]]:
             origPriceIndex = stack.pop()
             discount = prices[i]
             prices[origPriceIndex] -= discount
2. stack.append(i)
3. return prices

Solution 1: [Monotonic Stack] Increasing Stack of Pending Indices - Stack/Monotonic Stack Next Smaller Element

    def finalPrices(self, prices: List[int]) -> List[int]:

        # Discount Rule:
        # - each prices has a change to get a discount 1 time
        # - the first later price that is less than, subtract that from the price 

        # Monotonic Stack:
        # - stores prices that are waiting for a discount
        # - once a price gets its 1 time discount, remove from stack and update prices

        # [0] ... [i]
        # low     high

        # Stack Progression Example: [7, 4, 6, 2, 3]
        # - 7 pending => [7]
        # - 4 is 7's discount (7 - 4 = 3), 4 pending => [4]
        # - 6 pending => [4, 6]
        # - 2 resolves 6's discount (6-2=4), then resolves 4's discount (4-2=2), 2 pending (stack: [2])
        # - 3 pending => [2, 3]
        # - loop ends, 2 and 3 never found a discount, keep original prices

        # - discountedPrices = [3, 2, 4, 2, 3]

        # Pending Indices
        # sc: O(n)
        stack = []

        # tc: O(n)
        for i in range(len(prices)):

            # Discount Found:
            # - current price[i] is less than top of stack
            # - then we found discount for top of stack
            # - discount top of stack, pop price we just discounted, 
            #   and compare with new top if discount also applies
            while stack and prices[i] <= prices[stack[-1]]:

                # Grab original price index
                origPriceIndex = stack.pop()

                # Get discount
                discount = prices[i]

                # Reduce price by discount
                prices[origPriceIndex] -= discount

            # Push curr price to pending for a discount
            stack.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return prices

901. Online Stock Span ::1:: - Medium

Topics: Stack, Design, Monotonic Stack, Data Stream

Intro

Design an algorithm that collects daily price quotes for some stock and returns the span of that stock's price for the current day. The span of the stock's price in one day is the maximum number of consecutive days (starting from that day and going backward) for which the stock price was less than or equal to the price of that day. For example, if the prices of the stock in the last four days is [7,2,1,2] and the price of the stock today is 2, then the span of today is 4 because starting from today, the price of the stock was less than or equal 2 for 4 consecutive days. Also, if the prices of the stock in the last four days is [7,34,1,2] and the price of the stock today is 8, then the span of today is 3 because starting from today, the price of the stock was less than or equal 8 for 3 consecutive days. Implement the StockSpanner class: StockSpanner() Initializes the object of the class. int next(int price) Returns the span of the stock's price given that today's price is price.

InputOutput
["StockSpanner", "next", "next", "next", "next", "next", "next", "next"] [[], [100], [80], [60], [70], [60], [75], [85]][null, 1, 1, 1, 2, 1, 4, 6]

Constraints:

1 ≤ price ≤ 10^5

Almost 10^4 calls will be made to next

Abstraction

Given a stream of daily prices, return for each new price the number of consecutive days ending today (including today) whose prices were less than or equal to today's price.

(Previous greater: today's span stretches backward until the nearest earlier day with a strictly higher price, so we only care about where that blocking day sits)

(Stack: the most recent earlier day is the first one today can absorb, last in first out, so a stack holds the days that could still block a future day)

(Monotonic: any earlier day with a price less than or equal to today can never block a future day, since today blocks first, so the stack of remaining prices is always decreasing from bottom to top)

(Span Pairs: instead of storing indices, each entry carries the span it already covers, so absorbing an entry means adding its span and never re-walking the days it collapsed)

(Start with itself) span = 1 = "today always counts as at least one day"

(Check for a day to absorb) self.stack[-1][0] lte price = "is the price on top of the stack less than or equal to today's price?"

(Absorb the span) span += prevSpan = "this entry's whole run of days is now part of today's run, so add it and remove the entry from the stack"

(Push the pair) self.stack.append((price, span)) = "today is now the nearest price a future day must exceed, and it carries the full span it covers"

(Return the span) return span = "how many consecutive days back, including today, were at most today's price?"

Pseudocode

Sol 1: Decreasing Stack of (Price, Span) Pairs
1. def __init__(self):
    a. (self.stack = [])

2. def next(self, price: int) -> int:
    a. span = 1
    b. while self.stack and self.stack[-1][0] <= price:
         (prevPrice, prevSpan) = self.stack.pop()
         span += prevSpan
    c. self.stack.append((price, span))
    d. return span

Solution 1: [Monotonic Stack] Decreasing Stack of (Price, Span) Pairs - Stack/Monotonic Stack Online Stock Span

class StockSpanner:

    # Price Stream:
    # - Given a stream of prices
    # - For each index i, find find out how many consecutive days immediately preceding today 
    #   had prices less than or equal to today's price
    # - Consecutive days cannot be broken up by a more expensive day

    # Monotonic Stack (Price, Span):
    # - monotonic decreasing prices:
    
    # [0] ... [i]
    # high     low
    
    # - Tuples carry the span they 'own'
    #   representing their number of consecutive days (including itself) 

    # When today's price is greater than or equal to the price on top of the stack, 
    # that top entry can never again be the answer for any future day 
    # (today always dominates it looking backward), 
    # so we absorb its span into today's before pushing.

    # Why this works:
    # Absorbing spans instead of popping and forgetting means we
    # never have to re-walk days we've already collapsed. 
    # Each day is pushed once and popped at most once over the life of the object, 
    # so the amortized cost per call stays O(1).

    def __init__(self):

        # Tracking: (price, span)
        # sc: O(n)
        self.stack = []

    def next(self, price: int) -> int:

        # Spanning:
        # today always spans at least itself
        span = 1

        # Calculate Span:
        # - stack is non empty
        # - todays price is greater than the top of the stack
        # - top of the stack should be included into todays span 
        # - we can remove it from the top of the stack, since in the future
        #   it will be included within todays span
        while self.stack and self.stack[-1][0] <= price:

            # Popped span is now included within todays span,
            # no need to keep it on the stack anymore
            (prevPrice, prevSpan) = self.stack.pop()

            # Include span we popped into curr's span
            span += prevSpan

        # Monotonic Decreasing:
        # - Curr becomes new max needed to surpass to have a valid span
        self.stack.append((price, span))

        # Each (price, span) pushed/popped at most once across all calls to next()
        # tc: O(1) amortized
        return span

    # overall: tc O(n)
    # overall: sc O(n)

84. Largest Rectangle in Histogram ::2:: - Hard

Topics: Array, Stack, Monotonic Stack

Intro

Given an array of integers heights representing the histogram's bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram.

InputOutput
[2,4]4
[2,1,5,6,2,3]10

Constraints:

1 ≤ heights.length ≤ 105

0 ≤ heights[i] ≤ 104

Abstraction

Given an array of bar heights where each bar has width 1, return the area of the largest rectangle that fits inside the histogram.

(Boundaries: the tallest rectangle using a bar's full height stretches left and right until it hits a shorter bar, so every bar is waiting for the nearest shorter bar on each side)

(Stack: the most recent unresolved bar is the first one a new shorter bar can resolve, last in first out, so a stack holds the indices of bars whose right boundary is still unknown)

(Monotonic: a new bar resolves every waiting bar taller than it, so the waiting heights are always increasing from bottom to top, and after a pop the new top is exactly the popped bar's nearest shorter bar on the left)

(Indices: width is the distance between two boundaries, not a count of bars, so the stack stores indices instead of heights)

(Sentinel: a bar only gets its area when a shorter bar appears, so a 0 at the end forces every remaining bar to be resolved)

(Check for a right boundary) heights[stack[-1]] > heights[i] = "is the current bar shorter than the bar on top of the stack? if so, i is that bar's right boundary"

(Find the left boundary) stack[-1] after pop = "what is the nearest shorter bar to the left? if the stack is empty, nothing is shorter, so the boundary is an imaginary index -1"

(Compute the width) width = i - leftWallIndex - 1 = "how many bars sit strictly between the left and right boundaries?"

(Compute the area) tallWallHeight * width = "how large is the rectangle that uses the popped bar's full height across its whole span?"

(Track the best) max_area = max(max_area, tallWallHeight * width) = "is this rectangle the largest seen so far?"

(Push the pending bar) stack.append(i) = "the current bar is taller than everything below it, so it waits for its own right boundary"

(Flush the stack) heights.append(0) = "once the real bars end, which bars never found a shorter bar on the right? the sentinel resolves them"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Monotonic] Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance

    def largestRectangleArea(self, heights: List[int]) -> int:

        # Monotonic Stack: 
        # - maintains monotonic increasing heights
        # - when monotonic increasing stays true, 
        #   that implies new height is taller than all older heights
        # - when monotonic increasing is false, 
        #   that implies new height is smaller than at least 1 of the older heights
        # - if stack is non empty, that means we have multiple tall bars 
        #   that will serve to generate an area by covering shorter walls
        # - the width of the rectangle is distance between the index 
        #   the tall bar on top of the stack and index of the new shorter wall

        # Small Bar Popping Tall Bar Area Generation:
        # - a small bar to that breaks the monotonic increasing rule triggers area generation
        # - we know that the tallest bar on the stack, 'covers' all other bars on the stack
        # - we can use the tallest bar as the right boundary while using each smaller bars height,
        #   which gives us a valid width (tallest bar index - some smaller bar index)
        #   as well as a valid height (smaller bar height) which is guaranteed to be covered by the tallest bar,
        #   allowing use to use it as the height for the rectangle

        #                    *                                        A                         *        |                 *       
        #            *       *                               *        A                 A       A        |         *       *       
        #         *  *       *          *                 *  *        A  *           *  A       A  *     |      A  A       A  A 
        #      *  *  *       *     +    *       ==>    *  *  *        A  *   ==>  *  *  A       A  *     |   *  A  A       A  A 
        #     --------- ... ---        ---             --------- ... ------      --------- ... ------    |  --------- ... ------
        #    older     --> newer       new             0  1  2        6  7        0  1  2       6  7     |    0  1  2       6  7 
        #                                                                                                |
        #                                                 area from 6-6            area from 2-6         |  We do not generate area from 1-4
        #                                                 using height             using height          |  as 7 cannot act as its own right boundary
        #                                                 from 6 bar               from the 2 bar        |


        # Round 1: Generating areas while popping

        # - notice how we can't generate an area with a right boundary
        # - areas are not generated until a smaller bar appears
        # - tallest bar will serve as a right boundary for all areas generated per round

        # Pop() 1
        #                     *                                *                           A                                      
        #            *        *                        *       *                 *         A                           *          
        #         *  *        *        *            *  *       *  *           *  *         A  *                     *  *       * 
        #      *  *  *        *        *   ==>   *  *  *       *  *   ==>  *  *  *         A  *   ==> pop() ==>  *  *  *       * 
        #     --------- ... ----  +   ---       --------- ... ------       --------- ... -------      tall      --------- ... ---
        #      0  1  2        6  new   7         0  1  2       6  7          0  1  2       6  7       bar        0  1  2       7


        # Pop() 2
        #                                                                                                                 
        #            *                          *                      A                                           
        #         *  *          *            *  *       *           *  A        *                     *        * 
        #      *  *  *          *   ==>   *  *  *       *   ==>  *  *  A        *   ==> pop() ==>  *  *        * 
        #     --------- ... +  ---       --------- ... ---       --------- ... ---      tall      ------- ... ---
        #      0  1  2     new  7         0  1  2       7        0  1  2        7       bar        0  1        7


        # Final Result                                                                                                          
        #                                     
        #         *          *            *       * 
        #      *  *          *   ==>   *  *       *  
        #     ------ ... +  ---       ------ ... --- 
        #      0  1     new  7         0  1       7  


        # Round 2: Generating the walls while popping

        # Pop() 1
        #
        #         *       *                                *       *            *        * 
        #      *  *       *    +    *    ==> pop() ==>  *  *       *  *  ==> pop() ==>  *  *        *  A  A  *  
        #     ------ ... ---  new  ___       tall      ------ ... ------  ... ---
        #     0  1        7         8                   0  1       7  8   0  1        7

        # Diagram 2: Correcting the distance
        # If you recall, the distance between these two walls is actually:
        #
        #       *        *                      *        *               A  A  A  A  
        #    *  *        *    +    *    ==>  *  *        *  *   ==>   *  A  A  A  A  * 
        #   ---------------  new  ---       -----------------         -----------------

        # Diagram 2: A Stack Of Indexes
        # So to solve this problem, we keep the indexes on the stack not the heights,
        # in order to save the distance between the walls for when the time comes
        # to calculate the width of areas.

        # Diagram 2: Indexes
        #     A  A  A  A  
        #  *  A  A  A  A  * 
        # -----------------
        #  0  1  2  3  4  5                                

        # Diagram 2: Width Calculation
        # For any bar x
        #   - Left boundary = the nearest bar to the left that is shorter
        #   - Right boundary = the nearest bar to the right that is shorter
        #   - Width = (right boundary index - left boundary index) - 1
        # So for here: = (5 - 0) - 1 =  5 - 1 = 4 wide

        # Summary Popping Rule:
        # We keep popping as long as the stack has taller bars than the new smaller bar.
        # A taller bar implies that previous bars on the stack are covered  the new smaller bar is covered and can generate an area.
        # Once an area can be generated, we just need to calculate the width or the distance between the shorter bar and taller bar

        # Popping Rule Implies either:
        #
        #  1. The stack gets completely popped:
        #     Implies the new height candidate is the smallest height encountered so far 
        #     and was covered by all other previous tall bars.
        #     This allows the area being generated to span from 
        #     0 -> new_candidate = 0 -> i = i - 0: so width = i
        #     and the height to be the height of the new height candidate, so height = new height candidate
        #
        #  2. The stack does not get completely popped:
        #     Implies the new height candidate is taller than at least 1 older bar
        #     and is not covered by all other previous tall bars.
        #     This creates a left bound for the area being generated to span from
        #     left bound -> new candidate, left bound -> i = i - left_bound: so width = i - left_bound
        #     The height 
        #     After the area is generated, the new height candidate becomes the new top of the stack, 
        #     and is compared against new walls, which may or may not be taller or shorter.
        
        # Sentinel: 
        # With the rule that area is not calculated until a smaller bar appears, 
        # we need to add a 0 to flush the remaining bars on the stack, 
        # so that every bar is eventually processed regardless of height

        sentinel = 0
        heights.append(sentinel)

        # sc: stores indexes for up to n heights O(n)
        stack = []
        max_area = 0

        # tc: O(n)
        for i in range(len(heights)):

            # Monotonic increasing is broken: 
            # Check: if new bar breaks monotonic increasing order
            # Implies: The new candidate is shorter than at least 1 bar on the stack
            # Implies: We have found a right boundary
            # Rule: The taller bar can generate an area, pop() height for taller bar,
            # then check top of stack for the left boundary
            while stack and heights[stack[-1]] > heights[i]:
                
                # Grab taller bar index and height
                tallWallIndex = stack.pop()
                tallWallHeight = heights[tallWallIndex]

                # Check: if stack is empty after pop()
                # Implies: the new wall is shorter than all previous heights,
                # Implies: the left boundary goes up until an imaginary index -1 before the start
                if not stack:
                    # Actual rectangle is from 0 to i-1
                    # So the surrounding boundaries are at -1 and i:
                    # width = (right boundary) - (left boundary) - 1
                    #       = i - (imaginary index -1 before the start) - 1
                    #       = i - (-1) - 1
                    width = i - (-1) - 1

                # Check: Stack is not empty after pop()
                # Implies: top of stack can serve as left boundary
                else:
                    # Grab left boundary
                    leftWallIndex = stack[-1]

                    # Actual rectangle is from (leftWallIndex + 1) to (i-1)
                    # So the surrounding boundaries are at (leftWallIndex) and i
                    # width = (right boundary) - (left boundary) - 1
                    #       = i - (left wall on top of stack) - 1
                    #       = i - stack[-1] - 1
                    width = i - leftWallIndex - 1

                # Check: new max area
                max_area = max(max_area, tallWallHeight * width)

            # Monotonic increasing is maintained: 
            # Curr height is taller than all walls on the stack, there is no area to generate, append to stack and grab next new wall
            stack.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return max_area

Solution 2: [Monotonic] Reverse Iteration Index Math Imaginary Boundaries Surrounding Rectangles Game By Monotonic Stack Covering Heights Rule Implication - Stack/Monotonic Property Maintenance

    def largestRectangleArea(self, heights: List[int]) -> int:
        
        # Monotonic Stack: 
        # A stack that maintains monotonic increasing heights

        # Stack popping off taller bars:                                                                                                                                                                    
        #               *                                     *                                                
        #            *  *                                  *  *                 *                             
        #         *  *  *          *                    *  *  *   *          *  *   *          *   *          *  *
        #      *  *  *  *    +     *        ==>      *  *  *  *   *   =>  *  *  *   *   =>  *  *   *  ==>  *  *  *
        #     ------------   new  ---     pop off   ------------ ---     --------- ---     ------ ---     ---------
        #    taller bars   shorter bar    taller walls                                                final result

        # Reverse Iteration Notice:
        # The diagram is the same for left to right iteration and right to left iteration
        # even though we are grabbing the bars on the right vs the left.
        # That is because the stack itself cannot tell the difference and just does the same algorithm.
        # Thus, all that changes is the width calculation with the indexes

        # Index Math:
        # Follow the same approach as forward iteration of finding the imaginary boundaries

        n = len(heights)
        max_area = 0

        stack = []

        # tc: O(n)
        for i in range(n-1, -2, -1):

            # Sentinel: append a sentinel index of -1, height of 0
            curr_height = 0 if i == -1 else heights[i]

            # Monotonic increasing is broken: 
            # Check: if new bar breaks monotonic increasing order
            # Implies: The new candidate is shorter than at least 1 bar on the stack
            # Implies: We have found a right boundary
            # Rule: The taller bar can generate an area, pop() height for taller bar,
            # then check top of stack for the left boundary
            while stack and heights[stack[-1]] > curr_height:

                # Grab taller bar index and height
                index = stack.pop()
                height = heights[index]

                # Check: if stack is empty after pop()
                # Implies: the new wall is shorter than all previous heights,
                # Implies: the right boundary goes up until an imaginary index n after the end
                if not stack:
                    # Actual rectangle is from (i+1) to (n-1)
                    # So the surrounding boundaries are at i and n:
                    # width = (right boundary) - (left boundary) - 1
                    #       = (imaginary index) - i - 1
                    #       = n - i - 1
                    width = n - i - 1

                # Check: Stack is not empty after pop()
                # Implies: top of stack can serve as right boundary
                else:
                    # Grab right boundary
                    rightWallIndex = stack[-1]

                    # Actual rectangle is from (i+1) to (rightWallIndex - 1)
                    # So the surrounding boundaries are at i and rightWallIndex:
                    # width = (right boundary) - (left boundary) - 1
                    #       = (right wall on top of stack) - i - 1
                    #       = stack[-1] - i - 1
                    width = rightWallIndex - i - 1

                # check new area
                max_area = max(max_area, height * width)

            # if non-sentinel index, append
            if i >= 0:
                stack.append(i)

        # overall: tc O(n)
        # overall: sc O(n)
        return max_area

402. Remove K Digits ::1:: - Medium

Topics: String, Stack, Greedy, Monotonic Stack

Intro

Given string num representing a non-negative integer num, and an integer k, return the smallest possible integer after removing k digits from num.

InputOutput
num = "10", k = 2"0"
num = "10200", k = 1"200"
num = "1432219", k = 3"1219"

Constraints:

1 ≤ k ≤ num.length ≤ 105

num consists of only digits

num does not have any leading zeros except for 0 itself

Abstraction

Given a string of digits and an integer k, return the smallest number possible after removing exactly k digits.

(Greedy: the leftmost digits matter most, so whenever a digit is followed by a smaller digit, removing the larger one makes the number smaller than removing anything after it)

(Stack: the most recent kept digit is the first one a new smaller digit can replace, last in first out, so a stack holds the digits kept so far)

(Monotonic: a new digit pops every larger digit on top while removals remain, so the kept digits are always increasing from bottom to top, with the smallest digits as far left as possible)

(Leftover removals: if the whole string is already increasing, nothing was popped, and the largest digits sit at the right end, so the remaining k removals come off the end)

(Clean up: removing digits can leave leading zeros, which are not part of the number, and an empty result means the answer is 0)

(Check for a removable digit) stack and k > 0 and stack[-1] > digit = "do we still have removals left, and is the digit on top larger than the new digit?"

(Remove the larger digit) stack.pop(); k -= 1 = "this digit makes the number bigger than it needs to be, so drop it and use one removal"

(Push the digit) stack.append(digit) = "the new digit is now as far left as it can get, and it waits to be replaced by a later smaller digit"

(Trim the end) stack[:-k] = "if removals are left over, which digits are the largest? the rightmost ones, so cut them off"

(Strip leading zeros) ''.join(stack).lstrip('0') = "what number is left once leading zeros are dropped?"

(Handle empty) if not result: result = "0" = "if every digit was removed or zero, the answer is 0, not an empty string"

Pseudocode

  oh! pseudocode hasn't been written yet, try another card! :)

Solution 1: [Greedy] [Stack] Push Lower Digits To Left Of Stack Then Slice Via Monotonic Increasing Digits Stack - Stack/Monotonic Property Maintenance

    def removeKdigits(self, num: str, k: int) -> str:

        # Monotonic Stack:
        # A Stack that maintains increasing digits
        # To create the smallest number possible, we want our stack to keep smaller numbers
        # on the left side and larger numbers on the right:
        
        # Smaller number (left to right):
        # stack = [ 1 2 3 4 5 ]
        
        # Instead of larger number (right to left):
        # stack = [ 5 4 3 2 1 ]

        # A monotonic increasing stack allows us to create the smallest number possible
        # A monotonic decreasing stack allows us to create the largest number possible

        # So with a monotonic increasing stack, by ordering left to right,
        # we can slice at the end and grab as many numbers as we need or remove
        # more numbers (if we still have k left)
        
        # Slicing Grabbing:
        # "12345" [:3] -> "123"
        # 3 grabs the leftmost digits, creates the smallest number possible
        
        # Slicing Removal:
        # "12345" [:-2] -> "123"
        # -2 removes rightmost 2 digits, creates the smallest number possible

        # Greedy Summary:
        # The greedy removal is ensuring monotonic stack, 
        # as we want the smaller numbers to be to the left as much as possible, 
        # and we want to keep removing from the stack as long as we can (have k left)
        
        # sc: stack holds increasing digits up to n digits O(n)
        stack = []

        # tc: O(n)
        for digit in num:
            
            # Check: if stack is non empty, we have a candidate for removal
            # Check: if (k > 0) we still have digits to remove
            # Implies: if monotonic increasing broken, there is a larger digit to remove
            # Then: remove larger digit on stack to allow lower digit to be more to the left
            while stack and k > 0 and stack[-1] > digit:
                
                # Top of stack is larger than new digit, remove
                stack.pop()
                # Decrease remaining digits to remove
                k -= 1

            # Monotonic increasing valid:
            # New digit is as far to the left as possible
            stack.append(digit)

        # Building stack complete:
        # Digits are now ordered left to right with smallest being on the left
        # Check: if (k > 0) we still have digits to remove
        # Then: remove larger digits from the right side, to produce final smallest answer
        if k > 0:
            stack = stack[:-k]

        # Join all digits in the stack into a single string
        smallerNumber = ''.join(stack)

        # Clean answer:
        # Remove leading zeros "0000200" -> "200"
        result = smallerNumber.lstrip('0')

        # 0 Check:
        # if smallerNumber was all zeros, we will get an empty string after removing zeros
        # "0000" -> ""  
        # so check if empty and return "0"
        if not result:
            result = "0"

        # overall: tc O(n)
        # overall: sc O(n)
        return result