Jc-alt logo
jc
LeetCode: Stack

LeetCode: Stack

··
60 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 '()[]'

Abstract

Use a stack to track open parentheses. We we encounter a closed parenthesis make sure there is a matching open one.

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].

Abstract

We're designing abstract syntax tree that when execute, will execute the given operations in reverse polish notation.

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

232. Implement Queue using Stacks ::1:: - Easy

Topics: Stack, Design, Queue

Intro

Implement a first in first out (FIFO) queue using only two stacks. The implemented queue should support all the functions of a normal queue (push, peek, pop, and empty). Implement the MyQueue class: void push(int x) Pushes element x to the back of the queue. int pop() Removes the element from the front of the queue and returns it. int peek() Returns the element at the front of the queue. boolean empty() Returns true if the queue is empty, false otherwise. Notes: You must use only standard operations of a stack, which means only push to top, peek/pop from top, size, and is empty operations are valid. Depending on your language, the stack may not be supported natively. You may simulate a stack using a list or deque (double-ended queue) as long as you use only a stack's standard operations. Follow-up: Can you implement the queue such that each operation is amortized O(1) time complexity? In other words, performing n operations will take overall O(n) time even if one of those operations may take longer.

InputOutput
["MyQueue", "push", "push", "peek", "pop", "empty"] [[], [1], [2], [], [], []][null, null, null, 1, 1, false]

Constraints:

1 ≤ x ≤ 9

At most 100 calls will be made to push, pop, peek, and empty.

All the calls to pop and peek are valid.

Abstract

something!

Pseudocode

Sol 1: Lazy Transfer In Stack Out Stack
1. def __init__(self):
    a. (self.inStack = [])
    b. (self.outStack = [])

2. _transfer():
    a. if not self.outStack:
         while self.inStack:
             self.outStack.append(self.inStack.pop())

3. push(x):
    a. self.inStack.append(x)

4. pop():
    a. self._transfer()
    b. return self.outStack.pop()

5. peek():
    a. self._transfer()
    b. return self.outStack[-1]

6. empty():
    a. return not self.inStack and not self.outStack

Solution 1: [Stack] Lazy Transfer From In LIFO Stack To Out FIFO Stack [TC Opt] - Stack/Design Queue Using Stack

class MyQueue:

    def __init__(self):

        # Two Stacks (In LIFO / Out FIFO):
        # - In stack holds elements in LIFO order, stored here until transferred
        # - Out stack holds elements in FIFO order, which are passed by switching from LIFO to FIFO

        # Lazy Transfer:
        # - Only transfer when out FIFO stack is empty and we need some element
        # - Transfer all elements current in LIFO In Stack to FIFO Out Stack
        # - All elem moved In to Out at most once, transfer is O(1) amortized across calls

        # In stack: 
        # - LIFO which wait to be transferred
        # sc: O(n)
        self.inStack = []

        # Out stack:
        # - FIFO order via reversing via grabbing from the top of the LIFO stack
        # sc: O(n)
        self.outStack = []


    # Transfer:
    # tc: O(n)
    def _transfer(self) -> None:

        # Ran out of FIFO elements
        if not self.outStack:

            # Transfer entire LIFO element to FIFO stack
            while self.inStack:

                # Grab and push
                elem = self.inStack.pop()
                self.outStack.append(elem)

    # Push:
    # tc: O(1)
    def push(self, x: int) -> None:

        # Push to LIFO stack, will eventually be transferred
        self.inStack.append(x)

    # Pop:
    # tc: O(1) amortized, 
    # tc: occasional O(n) transfer across many future O(1) calls
    def pop(self) -> int:

        # Check if LIFO is empty, and transfer if necessary
        self._transfer()

        # LIFO guaranteed to have at least 1 element
        return self.outStack.pop()

    # Peek:
    # tc: O(1) amortized
    # tc: occasional O(n) transfer across many future O(1) calls
    def peek(self) -> int:

        # Check if LIFO is empty, and transfer if necessary
        self._transfer()

        # LIFO guaranteed to have at least 1 element
        return self.outStack[-1]

    # Empty:
    # tc: O(1)
    def empty(self) -> bool:

        # Both LIFO and FIFO are empty
        return not self.inStack and not self.outStack


    # overall: tc O(1) amortized, O(n)
    # overall: sc O(n)

155. Min Stack ::2:: - Medium

Topics: Stack, Design

Intro

Design a stack that supports push, pop, top, and retrieving the minimum element in constant time. Implement the MinStack class: MinStack() initializes the stack object. void push(int val) pushes the element val onto the stack. void pop() removes the element on the top of the stack. int top() gets the top element of the stack. int getMin() retrieves the minimum element in the stack. You must implement a solution with O(1) time complexity for each function.

InputOutput
["MinStack","push","push","push","getMin","pop","top","getMin"] [[],[-2],[0],[-3],[],[],[],[]][null,null,null,null,-3,null,0,-2]

Constraints:

-231 ≤ val ≤ 231 - 1

Methods pop, top and getMin operations will always be called on non-empty stacks.

At most 3 * 104 calls will be made to push, pop, top, and getMin.

Abstract

We need to design a stack that runs in O(1) time complexity for each main function.

Pseudocode

Sol 1: Tuple Stack Manual Size Pointer To Avoid Inbuilt Pop()

1. self.stack = [], self.size = 0

_overwriteCheck():
1. return self.size < len(self.stack)

push(val):
1. if self.size > 0:
    currMin = min(val, self.stack[self.size-1][1])
2. else:
    currMin = val
3. if self._overwriteCheck():
    self.stack[self.size] = (val, currMin)
4. else:
    self.stack.append((val, currMin))
5. self.size += 1

pop():
1. self.size -= 1

top():
1. return self.stack[self.size-1][0]

getMin():
1. return self.stack[self.size-1][1]


Sol 2: Stack Using Inbuilt Pop() and Push()

1. self.stack = []

push(val):
1. if self.stack:
    a. currMin = min(self.stack[-1][1], val)
2. else:
    a. currMin = val
3. self.stack.append((val, currMin))

pop():
1. self.stack.pop()

top():
1. return self.stack[-1][0]

getMin():
1. return self.stack[-1][1]

Solution 1: [Stack] Tuple Stack Manual Size Pointer To Avoid Inbuilt Pop() - Stack/Dynamic Programming State Compression

class MinStack:

    # MinStack:
    # Track 2 things
    #   - Top Of Stack
    #   - Min val

    # Push(), Pop(), Top(), GetMin():
    # - all in O(1)
    # - may need to update the above 2 details:

    # Stack:
    # Tuple Representing (Top Of Stack, Min At Level):
    
    #          (7, 1)       5
    #          (4, 1)       4
    #          (1, 1)       3
    #          (7, 5)       2
    #          (5, 5)       1
    #           Stack     Level

    def __init__(self):

        # Tuple Stack: (top of stack, min up to this level):
        # sc: O(n)
        self.stack = []
        self.size = 0
 
    # Helper:
    # If size pointer is behind actual length of stack, we overwrite a value when we push
    # If size pointer is accurate to length of stack, we simply append to the end of the stack 
    def _overwriteCheck(self):
        return self.size < len(self.stack)

    # Push():
    def push(self, val: int):

        # If some min exists, compare
        # else new value automatically update to new value
        if self.size > 0:
            currMin = min(val, self.stack[self.size-1][1])
        else:
            currMin = val
        
        # Overwrite, pointer is not at actual top of stack
        if self._overwriteCheck():
            self.stack[self.size] = (val, currMin)

        # No overwrite, append to end of stack
        else:
            self.stack.append((val, currMin))

        # Adjust pointer to new top of stack
        self.size += 1

    # Pop():
    def pop(self):

        # Adjust pointer to new top of stack,
        # allows us to pop without using the inbuilt pop(),
        # however, this now requires us to use an overwrite in push()
        # since actual stack memory size may differ from the pointer length
        self.size -= 1

    # Top():
    def top(self):

        # Grab val at top of stack
        return self.stack[self.size - 1][0]

    # GetMin():
    def getMin(self):

        # Grab min at current level
        return self.stack[self.size - 1][1]

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

Solution 2: [Stack] Stack Using Inbuilt Pop() and Push() - Stack/Dynamic Programming State Compression

class MinStack:

    # MinStack:
    # Track 2 things
    #   - Top Of Stack
    #   - Min val

    # Push(), Pop(), Top(), GetMin():
    # - all in O(1)
    # - may need to update the above 2 details:

    # Stack:
    # Tuple Representing (Top Of Stack, Min At Level):
    
    #          (7, 1)       5
    #          (4, 1)       4
    #          (1, 1)       3
    #          (7, 5)       2
    #          (5, 5)       1
    #           Stack     Level

    def __init__(self):
        
        # Tuple Stack: (top of stack, min up to this level):
        # sc: O(n)
        self.stack = []

    # Push():
    def push(self, val: int):

        # Compare new val to curr stack min
        if self.stack:
            currMin = min(self.stack[-1][1], val)
        else:
            currMin = val

        self.stack.append((val, currMin))

    # Pop():
    def pop(self):
        
        # Remove item at top of stack, 
        # new min automatically updates to new top
        self.stack.pop()

    # Top():
    def top(self):

        # View top of stack val
        return self.stack[-1][0]

    # GetMin():
    def getMin(self):

        # View top of stack min
        return self.stack[-1][1]

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

Abstract

Smashing stones but with a directional component. Each stone caries a weight Positives move to the right, negative to the left. Every stone moves at the same speed, 1 tick per interval, 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. One left mover can smash multiple right movers if its big enough. Use a stack to hold surviving right movers. When a left-mover shows up, keep colliding it against the top of the stack, smaller one explodes, equal sizes mutually explode, and bigger one survives and absorbs the rest.

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

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

Abstract

We need to compute the final price of every item after applying its discount.

Each item's discount is determined by the first item to its right whose price is less than or equal to the current item's price. If no such item exists, the item receives no discount.

The challenge is efficiently finding the first future price satisfying this condition for every item, then subtracting that value from the current 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

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.

Abstract

Given an array, for every num, determine what is the next greatest value.

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

Abstract

Given an array, for every num, determine what is the next greatest value. The array functions as a circular array.

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

Abstract

Given an array, for every num, find the number of steps until a higher num.

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

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

Abstract

Given a array of nums, for each num, determine how many the count of smaller or equal numbers in the past, excluding anything once we encounter a higher num. [20 100, 80, 60, 70, 85] Here, only 80, 60, 70 count, because we cut off at the 100 before we reach the 20.

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)

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

Abstract

Given a list of speeds, determine how many cars will catch up to another and end up bumper to bumper (fleet) before arriving to the destination.

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

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

Abstract

Given array of heights, find the area of the largest rectangle in the histogram.

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

Abstract

Remove k digits in a way so that resulting integer is as large as possible.

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