單調堆疊資料結構

Hashing, Stacks & QueuesPriority 4 of 5 — High value — a gap here costs you roundsHigh value 更新於 Sep 19, 2026
Section priorityPriority 5 of 5 — Must know — expect it in almost every loopMust knowPriority 4 of 5 — High value — a gap here costs you roundsHigh valuePriority 3 of 5 — Worth knowing — usually a variant of a must-know patternWorth knowingPriority 2 of 5 — Niche — read once, revisit only if a company is known to askNicheMarked on the sections that carry it — unmarked sections are background/reference.

範圍 — next greater/previous smaller/span/直方圖這類題目 — 堆疊本身保持有序,所以每個元素只被推入與彈出一次。 另見stack.md — 單純的 LIFO 題目;monotonic_queue.md — 滑動視窗版的對應物;heap.md — 當你要的是全域極值而不是鄰近極值時;greedy.md§2-10 裡那些「只推不彈」的偽堆疊,真正的歸屬在這裡。

LeetCode 題目清單

總覽

單調堆疊是一種特化的堆疊,內部元素永遠維持單調(嚴格遞增或嚴格遞減)的順序。它能有效率地解決 next greater/smaller 元素、直方圖面積,以及序列最佳化這類問題。

關鍵性質

  • 時間複雜度:大多數操作是 O(n)(每個元素只推入/彈出一次)
  • 空間複雜度:O(n),用來存堆疊
  • 核心想法:依序處理元素的同時,維持堆疊的單調性
  • 什麼時候用:找 next/previous greater/smaller 元素、直方圖題、序列最佳化

參考資料

題型分類

模式 1:Next/Previous Greater Element — LC 739

  • 描述:找出比目前元素大的下一個或前一個元素
  • 範例:LC 496(Next Greater Element I)、LC 503(Next Greater Element II)、LC 739(Daily Temperatures)
  • 模式:用遞減單調堆疊,遇到更大的元素就彈出

模式 2:Next/Previous Smaller Element — LC 84

  • 描述:找出比目前元素小的下一個或前一個元素
  • 範例:LC 84(Largest Rectangle)、LC 42(Trapping Rain Water)、LC 907(Sum of Subarray Minimums)
  • 模式:用遞增單調堆疊,遇到更小的元素就彈出

模式 3:直方圖與面積問題 — LC 84

  • 描述:用高度資訊算面積、矩形或體積
  • 範例:LC 84(Largest Rectangle in Histogram)、LC 42(Trapping Rain Water)、LC 85(Maximal Rectangle)
  • 模式:用單調堆疊找出邊界,再算邊界之間的面積

模式 4:序列順序與驗證 — LC 456

  • 描述:驗證序列、找出特定樣式,或維持順序限制
  • 範例:LC 456(132 Pattern)、LC 901(Online Stock Span)、LC 1856(Maximum Subarray Min-Product)
  • 模式:用堆疊維持序列性質並驗證樣式

模式 5:最佳化與最大/最小 — LC 1793

  • 描述:在最大或最小限制下找最佳解
  • 範例:LC 1944(Number of Visible People)、LC 2104(Sum of Subarray Ranges)、LC 1793(Maximum Score)
  • 模式:用單調性質維持最佳候選

模式 6:環狀陣列 — LC 503

  • 描述:處理環狀或循環的陣列問題
  • 範例:LC 503(Next Greater Element II)、LC 457(Circular Array Loop)
  • 模式:把陣列走兩遍,或用模運算搭配單調堆疊

模板與演算法

模板比較表

模板類型 使用情境 堆疊順序 什麼時候用
遞減堆疊 Next/Previous Greater 遞減 找比目前元素大的元素
遞增堆疊 Next/Previous Smaller 遞增 找比目前元素小的元素
直方圖面積 矩形/面積問題 遞增 用高度算面積
環狀陣列 循環問題 視情況 處理環狀序列
樣式驗證 序列驗證 視情況 驗證特定樣式
最佳化堆疊 最大/最小問題 視情況 維持最佳候選

通用模板

python
def monotonic_stack_template(arr):
    """
    Universal template for monotonic stack problems
    Modify the condition and processing logic based on problem requirements
    """
    stack = []  # Store indices or values
    result = []
    
    for i, val in enumerate(arr):
        # Pop elements that violate monotonic property
        while stack and should_pop(stack, val, i):
            # Process the popped element
            popped = stack.pop()
            process_popped_element(popped, i, result)
        
        # Add current element to stack
        stack.append(i)  # or val depending on problem
    
    # Process remaining elements in stack
    while stack:
        popped = stack.pop()
        process_remaining_element(popped, result)
    
    return result

def should_pop(stack, current_val, current_idx):
    """Define when to pop based on problem requirements"""
    # For next greater: return arr[stack[-1]] <= current_val
    # For next smaller: return arr[stack[-1]] >= current_val
    pass

def process_popped_element(popped_idx, current_idx, result):
    """Process element when it's popped (found its next greater/smaller)"""
    pass

def process_remaining_element(popped_idx, result):
    """Process elements remaining in stack at the end"""
    pass
java
// Java Universal Template
public int[] monotonicStackTemplate(int[] arr) {
    Stack<Integer> stack = new Stack<>();
    int[] result = new int[arr.length];
    
    for (int i = 0; i < arr.length; i++) {
        // Pop elements that violate monotonic property
        while (!stack.isEmpty() && shouldPop(stack, arr, i)) {
            int poppedIdx = stack.pop();
            processElement(poppedIdx, i, result, arr);
        }
        
        // Add current element to stack
        stack.push(i);
    }
    
    // Process remaining elements
    while (!stack.isEmpty()) {
        int poppedIdx = stack.pop();
        processRemainingElement(poppedIdx, result);
    }
    
    return result;
}

private boolean shouldPop(Stack<Integer> stack, int[] arr, int currentIdx) {
    // Define condition based on problem requirements
    return arr[stack.peek()] <= arr[currentIdx]; // For next greater
}

模板 1:Next Greater Element(遞減堆疊) — LC 496

python
def next_greater_element(nums):
    """
    Find next greater element for each element
    LC 496, LC 503, LC 739
    """
    n = len(nums)
    result = [-1] * n
    stack = []  # Store indices
    
    for i in range(n):
        # Pop smaller or equal elements
        while stack and nums[stack[-1]] < nums[i]:
            idx = stack.pop()
            result[idx] = nums[i]  # Found next greater
        
        stack.append(i)
    
    return result
java
// Java Template 1
public int[] nextGreaterElement(int[] nums) {
    int n = nums.length;
    int[] result = new int[n];
    Arrays.fill(result, -1);
    Stack<Integer> stack = new Stack<>();
    
    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
            result[stack.pop()] = nums[i];
        }
        stack.push(i);
    }
    
    return result;
}

模板 2:Next Smaller Element(遞增堆疊) — LC 84

python
def next_smaller_element(nums):
    """
    Find next smaller element for each element
    Used in LC 84, LC 42
    """
    n = len(nums)
    result = [-1] * n
    stack = []  # Store indices
    
    for i in range(n):
        # Pop greater or equal elements
        while stack and nums[stack[-1]] > nums[i]:
            idx = stack.pop()
            result[idx] = nums[i]  # Found next smaller
        
        stack.append(i)
    
    return result

模板 3:Largest Rectangle in Histogram — LC 84

python
def largest_rectangle_area(heights):
    """
    Find largest rectangle area in histogram
    LC 84, LC 85
    """
    stack = []  # Store indices
    max_area = 0
    heights.append(0)  # Add sentinel
    
    for i, h in enumerate(heights):
        # Pop taller bars and calculate area
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            width = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, height * width)
        
        stack.append(i)
    
    return max_area
java
// Java Template 3
public int largestRectangleArea(int[] heights) {
    Stack<Integer> stack = new Stack<>();
    int maxArea = 0;
    int n = heights.length;
    
    for (int i = 0; i <= n; i++) {
        int h = (i == n) ? 0 : heights[i];
        
        while (!stack.isEmpty() && heights[stack.peek()] > h) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        
        stack.push(i);
    }
    
    return maxArea;
}

模板 4:環狀陣列處理 — LC 503

python
def next_greater_circular(nums):
    """
    Find next greater element in circular array
    LC 503
    """
    n = len(nums)
    result = [-1] * n
    stack = []
    
    # Process array twice to handle circular nature
    for i in range(2 * n):
        # Pop smaller elements
        while stack and nums[stack[-1]] < nums[i % n]:
            idx = stack.pop()
            result[idx] = nums[i % n]
        
        # Only add indices from first pass
        if i < n:
            stack.append(i)
    
    return result

模板 5:堆疊中帶額外資訊

python
def monotonic_stack_with_info(nums):
    """
    Store additional information with stack elements
    Used for complex calculations
    """
    stack = []  # Store (index, value, additional_info)
    result = []
    
    for i, val in enumerate(nums):
        while stack and stack[-1][1] <= val:
            idx, old_val, info = stack.pop()
            # Process with additional information
            result.append(calculate_result(idx, i, old_val, val, info))
        
        # Calculate additional information for current element
        additional_info = calculate_info(val, stack)
        stack.append((i, val, additional_info))
    
    return result

模板 6:樣式驗證(132 Pattern) — LC 456

python
def find_132_pattern(nums):
    """
    Find 132 pattern in array
    LC 456
    """
    n = len(nums)
    if n < 3:
        return False
    
    stack = []  # Store potential k values (decreasing)
    second = float('-inf')  # The "2" in 132 pattern
    
    # Traverse from right to left
    for i in range(n - 1, -1, -1):
        if nums[i] < second:  # Found "1" < "2"
            return True
        
        # Pop smaller values and update second
        while stack and stack[-1] < nums[i]:
            second = stack.pop()
        
        stack.append(nums[i])
    
    return False

依模式分類的題目

以模式分類的題目清單

模式 1:Next/Previous Greater Element 題目

題目 LC # 關鍵技巧 難度 模板
Next Greater Element I 496 遞減堆疊 Easy 模板 1
Next Greater Element II 503 環狀陣列 Medium 模板 4
Daily Temperatures 739 計算距離 Medium 模板 1
Remove K Digits 402 貪婪 + 堆疊 Medium 模板 1
Remove Duplicate Letters 316 字典序 + 堆疊 Medium 模板 1
Sliding Window Maximum 239 單調雙端佇列 Hard 模板 1
Shortest Unsorted Array 581 兩趟堆疊 Medium 模板 1
Sum of Subarray Ranges 2104 next greater + smaller Medium 模板 1+2

模式 2:Next/Previous Smaller Element 題目

題目 LC # 關鍵技巧 難度 模板
Largest Rectangle in Histogram 84 計算面積 Hard 模板 3
Maximal Rectangle 85 二維直方圖 Hard 模板 3
Sum of Subarray Minimums 907 貢獻法 Medium 模板 2
Number of Valid Subarrays 1063 數較小元素 Medium 模板 2
Minimum Cost Tree From Leaf Values 1130 最佳合併 Medium 模板 2
Find the Most Competitive Subsequence 1673 選子序列 Medium 模板 2
Maximum Subarray Min-Product 1856 以最小值為樞紐 Medium 模板 2

模式 3:直方圖與面積題目

題目 LC # 關鍵技巧 難度 模板
Trapping Rain Water 42 計算水位 Hard 模板 2
Container With Most Water 11 也可用雙指標 Medium 模板 2
Maximal Rectangle 85 逐列直方圖 Hard 模板 3
Maximum Rectangle 221 DP + 直方圖 Medium 模板 3
Minimum Number of Taps 1326 區間覆蓋 Hard 模板 2
Constrained Subsequence Sum 1425 DP + 單調雙端佇列 Hard 模板 2

模式 4:序列順序與驗證題目

題目 LC # 關鍵技巧 難度 模板
132 Pattern 456 樣式偵測 Medium 模板 6
Online Stock Span 901 單調堆疊 Medium 模板 1
Score of Parentheses 856 巢狀結構 Medium 模板 5
Valid Parenthesis String 678 平衡驗證 Medium 模板 5
Minimum Add to Make Parentheses Valid 921 平衡計數 Medium 模板 5
Validate Stack Sequences 946 模擬序列 Medium 模板 5
Maximum Nesting Depth of Parentheses 1614 追蹤深度 Easy 模板 5
Minimum Remove to Make Valid Parentheses 1249 平衡 + 移除 Medium 模板 5

模式 5:最佳化與最大/最小題目

題目 LC # 關鍵技巧 難度 模板
Maximum Score of Good Subarray 1793 雙指標 + 堆疊 Hard 模板 2
Number of Visible People in Queue 1944 視線問題 Medium 模板 1
Car Fleet 853 抵達時間的滾動最大值 — 那個堆疊從來不彈出 Medium §2-10
Count Robot Groups 4045 同一套貪婪,但沒有終點 — 改比速度 Medium §2-10
Car Fleet II 1776 每台車各自的碰撞時間 — 真正需要彈出的堆疊 Hard §2-10
Buildings With Ocean View 1762 由右往左掃 Medium 模板 1
Find the Winner of Circular Game 1823 約瑟夫問題 Medium 模板 4
Maximum Width Ramp 962 索引差 Medium 模板 1
Steps to Make Array Non-decreasing 2289 堆疊扛著一個 dp 值 Medium 模板 11
Pancake Sorting 969 反轉操作 Medium 模板 1

模式 6:環狀陣列題目

題目 LC # 關鍵技巧 難度 模板
Next Greater Element II 503 走兩遍陣列 Medium 模板 4
Circular Array Loop 457 環偵測 Medium 模板 4
Design Circular Queue 622 環狀緩衝區 Medium 模板 4
Design Circular Deque 641 雙端環狀 Medium 模板 4

進階/混合模式題目

題目 LC # 關鍵技巧 難度 模板
Sum of Total Strength of Wizards 2281 多個堆疊 Hard 多個
Number of Ways to Rearrange Sticks 1866 組合數學 + 堆疊 Hard 模板 5
Basic Calculator 224 運算式求值 Hard 模板 5
Basic Calculator II 227 運算子優先序 Medium 模板 5
Basic Calculator III 772 完整運算式解析 Hard 模板 5
Evaluate Reverse Polish Notation 150 後序求值 Medium 模板 5
Decode String 394 巢狀解碼 Medium 模板 5
Find Duplicate Subtrees 652 樹的序列化 Medium 模板 5
Exclusive Time of Functions 636 模擬呼叫堆疊 Medium 模板 5
Minimum Window Subsequence 727 雙指標 + 堆疊 Hard 模板 5

題目難度分布

  • Easy(8 題):基本的 next greater/smaller、簡單驗證
  • Medium(28 題):最常見的難度,涵蓋各種模式
  • Hard(16 題):複雜的面積計算、進階最佳化

模板使用頻率

  • 模板 1(Next Greater):15 題
  • 模板 2(Next Smaller):12 題
  • 模板 3(直方圖):8 題
  • 模板 4(環狀):6 題
  • 模板 5(驗證/複雜):11 題
  • 多個模板混用:8 題

模式選擇策略

決策流程圖

text
Problem Analysis for Monotonic Stack:

1. Does the problem involve finding next/previous elements?
   ├── YES: Next/Previous GREATER elements?
   │   ├── YES: Use Template 1 (Decreasing Stack)
   │   │   ├── Array is circular? → Use Template 4 (Circular)
   │   │   └── Standard case → Template 1
   │   └── NO: Next/Previous SMALLER elements?
   │       ├── YES: Use Template 2 (Increasing Stack)
   │       └── NO: Continue to step 2
   └── NO: Continue to step 2

2. Does the problem involve heights/areas/rectangles?
   ├── YES: Rectangle area calculation?
   │   ├── YES: Use Template 3 (Histogram)
   │   └── NO: Water trapping/volume?
   │       └── YES: Use Template 2 (Next Smaller)
   └── NO: Continue to step 3

3. Does the problem involve sequence validation/patterns?
   ├── YES: Parentheses/brackets?
   │   ├── YES: Use Template 5 (Validation)
   │   └── NO: Specific pattern (like 132)?
   │       └── YES: Use Template 6 (Pattern Detection)
   └── NO: Continue to step 4

4. Does the problem involve optimization/max-min constraints?
   ├── YES: Multiple criteria optimization?
   │   ├── YES: Use Template 5 (Complex Info)
   │   └── NO: Simple max/min tracking?
   │       └── YES: Use Template 1 or 2
   └── NO: Continue to step 5

5. Does the problem involve circular arrays or cyclic behavior?
   ├── YES: Use Template 4 (Circular Processing)
   └── NO: Consider if monotonic stack is the right approach
       └── May need different data structure/algorithm

逐步分析題目

  1. 先看清核心需求

    • 查詢 next/previous 元素 → 模板 1、2、4
    • 計算面積/矩形 → 模板 3
    • 樣式驗證 → 模板 5、6
    • 最佳化問題 → 模板 1、2、5
  2. 決定堆疊順序

    • 要找較大的元素 → 遞減堆疊(彈出較小的)
    • 要找較小的元素 → 遞增堆疊(彈出較大的)
    • 面積計算 → 通常是遞增堆疊
    • 樣式偵測 → 視樣式而定
  3. 決定處理方向

    • 由左往右:最常見,也最自然
    • 由右往左:找「next」元素時有時比較好寫
    • 環狀:把陣列處理多遍
  4. 決定堆疊裡放什麼

    • 索引:需要位置資訊時
    • 值:只需要比大小時
    • Tuple:需要額外資訊時

模板選擇速查

題型 模板 堆疊內容 處理順序
Next Greater 模板 1 索引 由左往右
Next Smaller 模板 2 索引 由左往右
Previous Greater 模板 1 索引 由左往右
Previous Smaller 模板 2 索引 由左往右
直方圖面積 模板 3 索引 由左往右
環狀陣列 模板 4 索引 走兩遍
樣式偵測 模板 6 由右往左
複雜驗證 模板 5 Tuple 視情況

總結與速查

複雜度速查

操作 時間 空間 備註
推入堆疊 O(1) - 每個元素只推入一次
彈出堆疊 O(1) - 每個元素只彈出一次
整體演算法 O(n) O(n) 攤還線性時間
Next Greater/Smaller O(n) O(n) 單趟掃過陣列
直方圖面積 O(n) O(n) 帶堆疊的線性掃描
環狀陣列 O(n) O(n) 走兩趟,複雜度不變

模板速查

模板 模式 關鍵程式碼
模板 1 Next Greater while stack and nums[stack[-1]] < nums[i]
模板 2 Next Smaller while stack and nums[stack[-1]] > nums[i]
模板 3 直方圖 while stack and heights[stack[-1]] > h
模板 4 環狀 for i in range(2 * n)
模板 5 驗證 在堆疊中存額外資訊
模板 6 樣式偵測 由右往左,同時追蹤條件

常見模式與技巧

Next Greater Element 模式

python
# Standard next greater element
def next_greater_elements(nums):
    stack, result = [], [-1] * len(nums)
    for i, num in enumerate(nums):
        while stack and nums[stack[-1]] < num:
            result[stack.pop()] = num
        stack.append(i)
    return result

子陣列的貢獻法

python
# Count contribution of each element
def sum_subarray_mins(arr):
    n = len(arr)
    left = [-1] * n    # Previous smaller element
    right = [n] * n    # Next smaller element
    
    # Calculate left boundaries
    stack = []
    for i in range(n):
        while stack and arr[stack[-1]] >= arr[i]:
            stack.pop()
        left[i] = stack[-1] if stack else -1
        stack.append(i)
    
    # Calculate contribution
    result = 0
    for i in range(n):
        result += arr[i] * (i - left[i]) * (right[i] - i)
    return result % (10**9 + 7)

直方圖面積計算

python
# Largest rectangle with height as key
def largest_rectangle_area(heights):
    stack = []
    max_area = 0
    for i, h in enumerate(heights + [0]):
        while stack and heights[stack[-1]] > h:
            height = heights[stack.pop()]
            width = i if not stack else i - stack[-1] - 1
            max_area = max(max_area, height * width)
        stack.append(i)
    return max_area

解題步驟

  1. 步驟 1:判斷模式類型

    • 找關鍵字:next/previous、greater/smaller、面積、矩形
    • 確認有沒有環狀/循環的需求
    • 判斷是否需要驗證或樣式偵測
  2. 步驟 2:挑對模板

    • 用決策流程圖
    • 想清楚堆疊順序(遞增還是遞減)
    • 決定堆疊裡要存什麼資訊
  3. 步驟 3:實作核心邏輯

    • 準備好堆疊與結果容器
    • 寫出 while 迴圈,彈出條件要對
    • 妥善處理彈出的元素
    • 處理最後留在堆疊裡的元素
  4. 步驟 4:處理邊界情況

    • 空陣列
    • 只有一個元素
    • 所有元素都相同
    • 嚴格遞增/遞減的序列
  5. 步驟 5:最佳化與驗證

    • 確認時間複雜度是 O(n)
    • 檢查空間複雜度
    • 用範例輸入驗證
    • 必要時處理整數溢位

常見錯誤與提醒

常見錯誤

  • 堆疊順序搞反:next greater 題卻用遞增堆疊(應該用遞減)
  • 索引與值混淆:需要算距離時卻只存了值
  • 處理不完整:忘了處理最後留在堆疊裡的元素
  • 邊界問題:沒有妥善處理堆疊為空的情況
  • 環狀邏輯:環狀陣列處理不正確(漏了第二趟)
  • 條件寫錯:比較運算子用錯(< vs <=、> vs >=)

最佳實務

  • 需要位置資訊時,一律存索引
  • 哨兵值(例如 0)簡化邊界處理
  • 除非真的需要由右往左,否則由左往右處理
  • 變數名要清楚:用 stackresultcurrent_idx,不要用 sresi
  • 在 while 條件加註解,說明維持的單調性質
  • 先處理邊界情況,再寫主要演算法

面試技巧

  1. 辨識模式

    • 聽到「next greater/smaller」這類關鍵字就要有反應
    • 面積/矩形題常常用得上單調堆疊
    • 序列驗證題可能需要以堆疊為基礎的做法
  2. 解題流程

    • 先從暴力解開始,把題目搞懂
    • 判斷單調性質能不能把解法優化
    • 畫例子,把堆疊的行為視覺化
  3. 面試時的溝通

    • 解釋為什麼這題適合用單調堆疊
    • 用例子把堆疊的狀態走一遍給面試官看
    • 討論時間/空間複雜度的取捨
  4. 實作提醒

    • 從模板骨架開始寫
    • 把心力放在 while 條件寫對
    • 用簡單的例子測試(像 [2,1,2,4,3,1])
  5. 預期會有的追問

    • 有重複值怎麼辦?
    • 如果要的是 previous 而不是 next 呢?
    • 空間複雜度還能再優化嗎?
    • 怎麼延伸到二維問題?

相關主題

  • 堆疊:單調堆疊是堆疊這個資料結構的特化應用
  • 雙端佇列:滑動視窗最大值用的是單調雙端佇列
  • 雙指標:某些面積計算題的替代解法
  • 動態規劃:有些最佳化問題會把 DP 和單調堆疊搭在一起
  • 二分搜尋:在有序結構中找邊界
  • 線段樹:區間最大/最小值的進階查詢

LC 範例

2-1) Daily Temperatures (LC 739) — 單調遞減堆疊

堆疊存索引;遇到更暖的一天就彈出。

java
// LC 739 - Daily Temperatures
// IDEA: Monotonic decreasing stack — pop when current > stack top
// time = O(N), space = O(N)
public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length;
    int[] ans = new int[n];
    Deque<Integer> stack = new ArrayDeque<>(); // stores indices
    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
            int idx = stack.pop();
            ans[idx] = i - idx;
        }
        stack.push(i);
    }
    return ans;
}

2-2) Largest Rectangle in Histogram (LC 84) — 單調遞增堆疊

來了比較矮的柱子就彈出;用堆疊算出寬度後求面積。

java
// LC 84 - Largest Rectangle in Histogram
// IDEA: Monotonic increasing stack — pop and compute area on shorter bar
// time = O(N), space = O(N)
public int largestRectangleArea(int[] heights) {
    Deque<Integer> stack = new ArrayDeque<>();
    int maxArea = 0;
    for (int i = 0; i <= heights.length; i++) {
        int h = (i == heights.length) ? 0 : heights[i];
        while (!stack.isEmpty() && h < heights[stack.peek()]) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            maxArea = Math.max(maxArea, height * width);
        }
        stack.push(i);
    }
    return maxArea;
}

2-3) Next Greater Element I (LC 496) — 單調堆疊 + HashMap

先把 nums2 的 next greater 全部算好,再回答 nums1 的查詢。

java
// LC 496 - Next Greater Element I
// IDEA: Monotonic decreasing stack on nums2; store results in map
// time = O(M + N), space = O(M)
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
    Map<Integer, Integer> map = new HashMap<>(); // val -> next greater val
    Deque<Integer> stack = new ArrayDeque<>();
    for (int num : nums2) {
        while (!stack.isEmpty() && num > stack.peek()) {
            map.put(stack.pop(), num);
        }
        stack.push(num);
    }
    int[] ans = new int[nums1.length];
    for (int i = 0; i < nums1.length; i++) {
        ans[i] = map.getOrDefault(nums1[i], -1);
    }
    return ans;
}

2-4) Trapping Rain Water (LC 42) — 單調堆疊

來了比較高的柱子就彈出;接到的水 =(較小高度的差)* 寬度。

java
// LC 42 - Trapping Rain Water
// IDEA: Monotonic stack — pop when taller bar found, water fills between boundaries
// time = O(N), space = O(N)
public int trap(int[] height) {
    Deque<Integer> stack = new ArrayDeque<>();
    int water = 0;
    for (int i = 0; i < height.length; i++) {
        while (!stack.isEmpty() && height[i] > height[stack.peek()]) {
            int bottom = stack.pop();
            if (stack.isEmpty()) break;
            int left = stack.peek();
            int width = i - left - 1;
            int boundedHeight = Math.min(height[left], height[i]) - height[bottom];
            water += width * boundedHeight;
        }
        stack.push(i);
    }
    return water;
}

2-5) Next Greater Element II (LC 503) — 環狀單調堆疊

把陣列走兩遍(或用模運算)來處理環狀的 next greater 查詢。

java
// LC 503 - Next Greater Element II (circular array)
// IDEA: Monotonic stack — traverse 2n indices with modulo for circular effect
// time = O(N), space = O(N)
public int[] nextGreaterElements(int[] nums) {
    int n = nums.length;
    int[] ans = new int[n];
    Arrays.fill(ans, -1);
    Deque<Integer> stack = new ArrayDeque<>();
    for (int i = 0; i < 2 * n; i++) {
        while (!stack.isEmpty() && nums[i % n] > nums[stack.peek()]) {
            ans[stack.pop()] = nums[i % n];
        }
        if (i < n) stack.push(i);
    }
    return ans;
}

2-6) Online Stock Span (LC 901) — 單調遞減堆疊

把先前 <= 目前價格的都彈掉;span = 距離上一個更高價過了幾天。

java
// LC 901 - Online Stock Span
// IDEA: Monotonic decreasing stack storing [price, span] pairs
// time = O(1) amortized per call, space = O(N)
class StockSpanner {
    Deque<int[]> stack = new ArrayDeque<>(); // [price, span]
    public int next(int price) {
        int span = 1;
        while (!stack.isEmpty() && stack.peek()[0] <= price) {
            span += stack.pop()[1];
        }
        stack.push(new int[]{price, span});
        return span;
    }
}

2-7) Sum of Subarray Minimums (LC 907) — 單調堆疊

對每個元素,找出它是最小值的左右邊界;用單調堆疊做。

java
// LC 907 - Sum of Subarray Minimums
// IDEA: Monotonic stack — for each element find left & right span as minimum
// time = O(N), space = O(N)
public int sumSubarrayMins(int[] arr) {
    int n = arr.length;
    int MOD = 1_000_000_007;
    int[] left = new int[n], right = new int[n];
    Deque<Integer> stack = new ArrayDeque<>();
    // left[i] = distance to previous smaller element
    for (int i = 0; i < n; i++) {
        while (!stack.isEmpty() && arr[stack.peek()] >= arr[i]) stack.pop();
        left[i] = stack.isEmpty() ? i + 1 : i - stack.peek();
        stack.push(i);
    }
    stack.clear();
    // right[i] = distance to next smaller or equal element
    for (int i = n-1; i >= 0; i--) {
        while (!stack.isEmpty() && arr[stack.peek()] > arr[i]) stack.pop();
        right[i] = stack.isEmpty() ? n - i : stack.peek() - i;
        stack.push(i);
    }
    long ans = 0;
    for (int i = 0; i < n; i++) ans = (ans + (long) arr[i] * left[i] * right[i]) % MOD;
    return (int) ans;
}

貢獻法 — 把 left[i] / right[i] 視覺化(Python) Priority 5 of 5 — Must know — expect it in almost every loop

leetcode_python/Math/sum-of-subarray-minimums.py

核心想法: 每個子陣列都恰好有一個最小值,所以不要去列舉子陣列,而是反過來問*「有多少個子陣列的最小值是 arr[i]?」* — 然後把 arr[i] * count 全部加起來。

對每個索引 i,這個數量會拆成兩個互相獨立的選擇:

text
        left choices              right choices
           <---->                    <----->
   ┌───────────────────────────────────────────────┐
   │  ...  PSE   .   .   .   [i]   .   .   .   NSE   │      arr
   └───────────────────────────────────────────────┘
              ^                          ^
        previous smaller           next smaller-or-equal
        element (strict >=)        element (strict >)

   left[i]  = i - PSE     ← # of left endpoints that keep arr[i] as min
   right[i] = NSE - i     ← # of right endpoints that keep arr[i] as min

   count(i) = left[i] * right[i]
   contribution = arr[i] * left[i] * right[i]
  • 一個子陣列要讓 arr[i] 當最小值,就必須起點落在 (PSE, i]終點落在 [i, NSE)
  • 兩個範圍互相獨立 → 相乘。

處理重複值(避免重複計算): 左邊那趟用 >=、右邊那趟用 >(刻意不對稱)。這樣相等的值只會被算在其中一側。

python
# python
# LC 907 - Sum of Subarray Minimums (contribution method)
# time = O(n), space = O(n)
MOD = 10**9 + 7
n = len(arr)
left  = [0] * n   # left[i]  = distance to previous smaller element
right = [0] * n   # right[i] = distance to next smaller-or-equal element

# --- LEFT pass: distance to Previous Smaller Element (pop on >=) ---
mono_st = []
for i in range(n):
    val = arr[i]
    # Pop elements that are greater than OR EQUAL to current val
    while mono_st and arr[mono_st[-1]] >= val:
        mono_st.pop()   # these can't be the left boundary of arr[i]

    # If stack empty -> val is the smallest so far, boundary is index -1
    #   left choices = i - (-1) = i + 1
    # Else -> boundary is the surviving stack top (the PSE)
    #   left choices = i - mono_st[-1]
    left[i] = i + 1 if not mono_st else i - mono_st[-1]
    mono_st.append(i)

# --- RIGHT pass: distance to Next Smaller Element (pop on >) ---
mono_st = []
for i in range(n - 1, -1, -1):
    val = arr[i]
    while mono_st and arr[mono_st[-1]] > val:   # strict > here
        mono_st.pop()
    right[i] = n - i if not mono_st else mono_st[-1] - i
    mono_st.append(i)

ans = 0
for i in range(n):
    ans = (ans + arr[i] * left[i] * right[i]) % MOD

堆疊為空時為什麼 left[i] = i + 1 堆疊為空代表左邊沒有任何元素比 arr[i] 小 — arr[i] 主宰了整個前綴。想像中的左邊界落在索引 -1,所以左邊的選擇涵蓋索引 0..i,也就是 i - (-1) = i + 1

arr = [3, 1, 2, 4] 上的圖解追蹤:

text
i=0 val=3 : stack empty              -> left[0] = 0-(-1) = 1   stack=[0]
i=1 val=1 : arr[0]=3 >= 1 -> pop 0
            stack empty              -> left[1] = 1-(-1) = 2   stack=[1]
i=2 val=2 : arr[1]=1 >= 2? no        -> left[2] = 2-1     = 1   stack=[1,2]
i=3 val=4 : arr[2]=2 >= 4? no        -> left[3] = 3-2     = 1   stack=[1,2,3]

left  = [1, 2, 1, 1]
right = [1, 3, 2, 1]   (symmetric backward pass with strict >)

contribution = 3*1*1 + 1*2*3 + 2*1*2 + 4*1*1 = 3 + 6 + 4 + 4 = 17  ✓

2-8) Remove K Digits (LC 402) — 單調遞增堆疊

維持遞增堆疊;來了比較小的數字就把前面的移掉。

java
// LC 402 - Remove K Digits
// IDEA: Greedy + monotonic increasing stack — remove larger digits greedily
// time = O(N), space = O(N)
public String removeKdigits(String num, int k) {
    Deque<Character> stack = new ArrayDeque<>();
    for (char c : num.toCharArray()) {
        while (k > 0 && !stack.isEmpty() && stack.peek() > c) {
            stack.pop(); k--;
        }
        stack.push(c);
    }
    while (k-- > 0) stack.pop(); // remove from top if k still > 0
    // reconstruct result in correct order (bottom to top of stack)
    Deque<Character> result = new ArrayDeque<>(stack);
    StringBuilder sb = new StringBuilder();
    boolean leadingZero = true;
    while (!result.isEmpty()) {
        char c = result.pollFirst();
        if (leadingZero && c == '0') continue;
        leadingZero = false;
        sb.append(c);
    }
    return sb.length() == 0 ? "0" : sb.toString();
}

2-9) Maximal Rectangle (LC 85) — 直方圖 + 單調堆疊

逐列算出直方圖高度;每一列套用 LC 84 的最大矩形邏輯。

java
// LC 85 - Maximal Rectangle
// IDEA: For each row build histogram; apply largestRectangleArea (LC 84) logic
// time = O(M*N), space = O(N)
public int maximalRectangle(char[][] matrix) {
    if (matrix.length == 0) return 0;
    int n = matrix[0].length, maxArea = 0;
    int[] heights = new int[n];
    for (char[] row : matrix) {
        for (int j = 0; j < n; j++)
            heights[j] = row[j] == '0' ? 0 : heights[j] + 1;
        maxArea = Math.max(maxArea, largestRectangle(heights));
    }
    return maxArea;
}
private int largestRectangle(int[] heights) {
    Deque<Integer> stack = new ArrayDeque<>();
    int max = 0;
    for (int i = 0; i <= heights.length; i++) {
        int h = i == heights.length ? 0 : heights[i];
        while (!stack.isEmpty() && h < heights[stack.peek()]) {
            int height = heights[stack.pop()];
            int width = stack.isEmpty() ? i : i - stack.peek() - 1;
            max = Math.max(max, height * width);
        }
        stack.push(i);
    }
    return max;
}

2-10) 車隊家族(LC 853 / 4045 / 1776)— 什麼時候「堆疊」其實是貪婪 Priority 4 of 5 — High value — a gap here costs you rounds

leetcode_python/Stack/car-fleet.pyleetcode_python/Stack/count-robot-groups.py

這一節講的是反面教材。 LC 853 一向被當成堆疊題教,這個 repo 的 README 也把它標成 **stack** — 但它的堆疊從來不彈出,所以它其實是穿著堆疊外衣的兩變數貪婪。這三題裡只有 LC 1776 真的需要堆疊。能說出是哪一題、以及為什麼,才是這節真正的價值。

三題共通的性質:前方說話算話

text
nobody can pass the entity ahead of it
  -> you can only ever merge with the entity DIRECTLY ahead
  -> a merged group moves at the FRONT member's speed (the rear one slows down)

so: scan RIGHT -> LEFT (front -> back), and the survivors appear in order

最後那一行,就是為什麼 853 和 4045 只需要一個「目前最前面的存活者」變數:存活者一旦確定就不會再被回頭修改 — 而堆疊只有在你必須回頭找更舊的候選者時,才配叫做堆疊。

LC 853 Car Fleet — 那個從來不彈出的堆疊

這題有終點 target,所以每台車都可以塌縮成一個數字 — 假設前方完全沒人擋路時,它抵達終點的時間:

text
t_i = (target - position[i]) / speed[i]

後車會併入前面的車隊,條件是它抵達的時間不會更晚,也就是 t_i <= max(前方所有人的 t)。於是由前往後排序,數「新的最大值出現幾次」就好:

python
# python
# LC 853 - Car Fleet  (the textbook "stack" version)
# time = O(n log n), space = O(n)
def carFleet(target, position, speed):
    st = []
    for p, s in sorted(zip(position, speed), reverse=True):   # front -> back
        t = (target - p) / s
        if not st or t > st[-1]:      # cannot catch the fleet ahead -> new fleet
            st.append(t)
        # else: t <= st[-1], it catches up and is absorbed — nothing to record
    return len(st)

注意這個迴圈從來沒有彈出過任何東西。所以 st 一定遞增,st[-1] 就只是目前看過最大的 t,而 len(st) 就只是那個最大值被更新過幾次。兩個變數就能做完全一樣的事:

python
# python
# LC 853 - the same algorithm, stack removed
# time = O(n log n) (the sort), space = O(1)
def carFleet(target, position, speed):
    cnt, mx = 0, 0.0
    for p, s in sorted(zip(position, speed), reverse=True):   # front -> back
        t = (target - p) / s
        if t > mx:                    # arrives later than anyone ahead -> its own fleet
            cnt += 1
            mx = t
    return cnt
java
// java
// LC 853 - Car Fleet, running-max greedy
// time = O(N log N), space = O(N) for the index array (O(1) beyond the sort)
public int carFleet(int target, int[] position, int[] speed) {
    int n = position.length;
    Integer[] idx = new Integer[n];
    for (int i = 0; i < n; i++) idx[i] = i;
    Arrays.sort(idx, (a, b) -> position[b] - position[a]);   // front -> back
    int cnt = 0;
    double mx = 0.0;
    for (int i : idx) {
        double t = (double) (target - position[i]) / speed[i];
        if (t > mx) { cnt++; mx = t; }
    }
    return cnt;
}

兩種寫法在隨機測資上結果一致 — 本來就該一致,因為它們是同一個演算法。挑你在壓力下寫得出來的那一種就好;只是不要聲稱那個堆疊有在做事。

LC 4045 Count Robot Groups — 同一套貪婪,只是沒有終點

機器人會一直往前跑,而且多了一個合併門檻 distance。沒有終點就代表不存在抵達時間,所以每個機器人沒有單一數字可以比較 — 狀態是這個群組「最右側機器人」的 (position, speed) 這一對。機器人 i 會被吸收,只要下面任一個條件成立:

text
position[i+1] - position[i] <= distance   -> already touching at t = 0
speed[i] > cur_s                          -> closing on the front group, and with
                                             infinite time ANY positive closing rate
                                             eventually eats ANY gap
python
# python
# LC 4045 - Count Robot Groups   (position is given sorted, so no sort is needed)
# IDEA: right -> left; `cur_s` is the speed of the frontmost group that survived.
# time = O(n), space = O(1)
def countGroups(position, speed, distance):
    n = len(position)
    cur_s = speed[n - 1]           # the frontmost robot is always a group on its own
    cnt = 1
    for i in range(n - 2, -1, -1):
        # touching test -> the NEIGHBOUR;  closing test -> the FRONT group
        if position[i + 1] - position[i] <= distance or speed[i] > cur_s:
            continue               # absorbed
        cnt += 1
        cur_s = speed[i]           # a new frontmost survivor
    return cnt

陷阱:這兩個測試用的是不同的參考對象。 合併時採用的是最右側機器人的狀態,所以有人從後面加入時 cur 完全不動 — 但 t = 0 的那些合併是同時發生的,而且是透過鄰居一路串起來的:

text
position = [18,19,22,24], distance = 3     gaps: 1, 3, 2  -> every pair touches
all four collapse at t = 0, even though 18 is 6 away from the group's position 24

t = 0 的接觸測試拿去和 cur_p 比(而不是和 position[i+1] 比),這些串接就會無聲無息地消失。而速度測試單獨拿去和 cur 比是安全的:如果機器人 i+1 之所以存活是因為它比前方更快,而 i 又比它更快,那 i 一定也比前方更快 — 追得上鄰居,就一定追得上前方那個群組。

853 與 4045 逐項對照

LC 853 Car Fleet LC 4045 Count Robot Groups
時間範圍 終點在 target 沒有終點 — 機器人永遠跑下去
每個實體的狀態 一個純量t = (target - p) / s (position, speed) 這一對;不存在純量
合併判定 t_i <= max(前方的 t) — 比時間 speed[i] > cur_s — 比速度
合併門檻 貼到零距離 間距 <= distance外加 t = 0 的接觸規則
輸入順序 位置沒排序 → 必須先排序 position 已排序 → 不用排序
合併後採用 的那台(也就是前車)的速度 最右側機器人的位置與速度
複雜度 O(n log n)/排序之外 O(1) O(n)/O(1)
需要堆疊嗎? 不需要 — 只推不彈 不需要 — 兩個變數

為什麼 853 有純量、4045 沒有,是這節最該帶走的一個想法:

  • 853 有期限,所以「後車追不追得上前車?」就變成「它抵達得會不會更晚?」— 一台車一個數字的比較。兩台車可以一路互相接近,卻仍然算成兩個車隊,因為它們相遇的地點會在 target 之後。是期限造出了那個純量,也是那個純量讓滾動最大值成立。
  • 4045 沒有期限,所以只有「有沒有在拉近」這件事重要:時間無限長的話,speed[i] > cur_s 本身就已經是答案了 — 根本沒有算式要算,因為沒有人會抵達任何地方。

LC 1776 Car Fleet II — 家族裡真正需要堆疊的那一題

同一條路、同樣「前方說話算話」,但問題換了:要回報每一台車的碰撞時間,而不是一個數量。這就打破了「單一存活者」這個不變式,因為前面那台車自己也會在某個已知時間 ans[j] 被吸收掉:

text
if car i reaches car j LATER than j's own collision time
    -> j is already gone when i gets there
    -> i's real target is whatever is further ahead
    -> back up to an older candidate        <- THIS is a pop
python
# python
# LC 1776 - Car Fleet II: ans[i] = when car i collides with the car ahead (-1 = never)
# IDEA: right -> left monotonic stack of cars still catchable. Pop a candidate that
#       cannot be caught, or that dies before we reach it.
# time = O(n), space = O(n)
def getCollisionTimes(cars):
    n = len(cars)
    ans = [-1.0] * n
    st = []                                     # indices, front -> back
    for i in range(n - 1, -1, -1):
        p, s = cars[i]
        while st:
            j = st[-1]
            pj, sj = cars[j]
            # (a) not faster than j -> can never reach it
            # (b) reaching j takes longer than j survives -> aim further ahead
            if s <= sj or (ans[j] > 0 and (pj - p) / (s - sj) >= ans[j]):
                st.pop()
            else:
                break
        if st:
            j = st[-1]
            ans[i] = (cars[j][0] - p) / (s - cars[j][1])
        st.append(i)
    return ans

# cars = [[1,2],[2,1],[4,3],[7,2]] -> [1.0, -1.0, 3.0, -1.0]
# cars = [[3,4],[5,4],[6,3],[9,1]] -> [2.0, 1.0, 1.5, -1.0]

帶進面試房間的判準

題目要你給的是 前方的行為是 該用什麼結構
存活群組的數量(853、4045) 永久的 — 存活者不會被回頭修改 一兩個變數(貪婪)
每個元素各自的時間或值(1776、2289) 暫時的 — 前方自己也會在已知時間死掉 真正的堆疊,而且會彈出

所以在面試官面前誠實的講法是:「853 和 4045 是同一套由右往左的貪婪 — LC 853 教科書解法的那個堆疊從來不彈出,所以我會改成維護一個滾動最大值。1776 才是堆疊真正派上用場的地方,因為已經算出來的答案有可能被推翻。」

2-11) Asteroid Collision (LC 735) — 堆疊模擬

向右飛的留在堆疊上;向左飛的一直和頂端相撞,直到穩定。

java
// LC 735 - Asteroid Collision
// IDEA: Stack — simulate collisions between right (+) and left (-) asteroids
// time = O(N), space = O(N)
public int[] asteroidCollision(int[] asteroids) {
    Deque<Integer> stack = new ArrayDeque<>();
    for (int a : asteroids) {
        boolean alive = true;
        while (alive && a < 0 && !stack.isEmpty() && stack.peek() > 0) {
            if (stack.peek() < -a) { stack.pop(); }      // stack top destroyed
            else if (stack.peek() == -a) { stack.pop(); alive = false; } // both destroyed
            else alive = false;                             // incoming destroyed
        }
        if (alive) stack.push(a);
    }
    int[] res = new int[stack.size()];
    for (int i = res.length - 1; i >= 0; i--) res[i] = stack.pop();
    return res;
}

2-12) Sum of Subarray Ranges (LC 2104) — 雙單調堆疊(貢獻法)

sum(ranges) = sum(subarray maxs) − sum(subarray mins)。最大值與最小值各跑一趟單調堆疊;每彈出一個元素,就算出它以最大/最小值的身分主宰了多少個子陣列。

核心想法

text
range(subarray) = max − min
sum(all ranges) = sum(all subarray maxs) − sum(all subarray mins)

對每個元素 nums[mid],找出它的主宰邊界:

  • 左邊界 L — 前一個會讓 nums[mid] 失去最大/最小身分的元素索引(沒有就是 -1
  • 右邊界 R — 下一個會取代它的元素索引(沒有就是 n

nums[mid] 擔任最大/最小值的子陣列數量:

text
count = (mid − L) × (R − mid)
contribution = nums[mid] × count

哨兵迴圈i0 跑到 n(含)。當 i == n 時,用 n 當右邊界把堆疊裡剩下的索引全部清掉。

對重複值安全的邊界規則(避免相等元素被重複計算):

  • 最大值那趟:nums[mid] < nums[i] 時彈出(嚴格);左邊界是上一個大於或等於的元素。
  • 最小值那趟:nums[mid] > nums[i] 時彈出(嚴格);左邊界是上一個小於或等於的元素。

圖解追蹤 — [1, 3, 2] 的最大值那趟

text
Decreasing stack (max contribution)

i=0: push 0         stack=[0]
i=1: nums[0]=1 < nums[1]=3 → pop mid=0
       left=-1, right=1
       contrib = 1 * (0-(-1)) * (1-0) = 1*1*1 = 1
     push 1          stack=[1]
i=2: nums[1]=3 > nums[2]=2, no pop
     push 2          stack=[1,2]
i=3 (sentinel): flush
     pop mid=2: left=1, right=3  → 2*(2-1)*(3-2) = 2
     pop mid=1: left=-1, right=3 → 3*(1-(-1))*(3-1) = 12

max_sum = 1 + 2 + 12 = 15

min pass (increasing stack) → min_sum = 10

answer = 15 − 10 = 5  ✓
verify: [1]=0,[3]=0,[2]=0,[1,3]=2,[3,2]=1,[1,3,2]=2 → sum = 5

模式(Python)

python
# python
# LC 2104 - Sum of Subarray Ranges
# IDEA: sum(ranges) = sum(subarray maxs) - sum(subarray mins)
#       Contribution method via monotonic stack — one pass per role
# time = O(N), space = O(N)
def subArrayRanges(nums):
    n = len(nums)

    def contribution(is_max):
        stack = []
        total = 0
        for i in range(n + 1):          # sentinel: i == n flushes remaining
            while stack and (
                i == n or
                (nums[stack[-1]] < nums[i] if is_max else nums[stack[-1]] > nums[i])
            ):
                mid = stack.pop()
                left  = stack[-1] if stack else -1   # previous boundary index
                right = i                            # current index = right boundary
                total += nums[mid] * (mid - left) * (right - mid)
            stack.append(i)
        return total

    return contribution(True) - contribution(False)

模式(Java)

java
// java
// LC 2104 - Sum of Subarray Ranges
// IDEA: sum(ranges) = sum(subarray maxs) - sum(subarray mins)
//       Contribution method: for each element count subarrays where it's max/min
// time = O(N), space = O(N)
public long subArrayRanges(int[] nums) {
    return contribution(nums, true) - contribution(nums, false);
}

private long contribution(int[] nums, boolean isMax) {
    int n = nums.length;
    Deque<Integer> stack = new ArrayDeque<>();
    long total = 0;

    for (int i = 0; i <= n; i++) {          // i == n is the sentinel flush
        while (!stack.isEmpty()) {
            int mid = stack.peek();
            boolean shouldPop = (i == n) ||
                (isMax ? nums[mid] < nums[i] : nums[mid] > nums[i]);
            if (!shouldPop) break;
            stack.pop();
            int left  = stack.isEmpty() ? -1 : stack.peek(); // prev boundary
            int right = i;                                    // next boundary
            total += (long) nums[mid] * (mid - left) * (right - mid);
        }
        stack.push(i);
    }
    return total;
}

雙堆疊邏輯總結

趟次 堆疊類型 彈出條件 算出什麼
最大值那趟 單調遞減 nums[mid] < nums[i] 所有子陣列最大值的總和
最小值那趟 單調遞增 nums[mid] > nums[i] 所有子陣列最小值的總和
兩趟都有 i = n 的哨兵 一律清空 處理靠右邊界的元素

相似題目

題目 LC# 關鍵差異
Sum of Subarray Ranges 2104 max_sum − min_sum;兩趟單調堆疊
Sum of Subarray Minimums 907 只算最小值的貢獻;單趟遞增堆疊
Maximum Subarray Min-Product 1856 最小值貢獻 × 子陣列和;前綴和 + 堆疊
Sum of Total Strength of Wizards 2281 最小值 × 和的和;前綴和的前綴和 + 堆疊
Largest Rectangle in Histogram 84 面積 = 高 × 寬;遇到較矮的柱子彈出
Number of Visible People in Queue 1944 每個元素彈出的次數就是答案

2-13) Longest Absolute File Path (LC 388) — 以巢狀深度為索引的堆疊 Priority 5 of 5 — Must know — expect it in almost every loop

模板 7:深度堆疊。 這個堆疊不是按單調,而是按深度單調:stack[d] 永遠存著深度 d 的累積路徑長度。處理深度為 d 的那一行之前,先彈到 stack.size() == d,就把剛結束的兄弟分支全部丟掉了。

核心想法

text
"dir\n\tsub1\n\t\tfile.ext\n\tsub2"

line          depth   pop until size==depth   stack (path lengths, '/' included)
dir             0     []                      [4]            "dir/"
  sub1          1     [4]                     [4, 9]         "dir/sub1/"
    file.ext    2     [4, 9]                  (file → no push, len = 9 + 8 = 17)
  sub2          1     pop 9 → [4]             [4, 9]
  • depth = 開頭 \t 的數量;剩下的部分就是名稱。
  • 目錄會推入 parentLen + name.length() + 1+1/ 分隔符)。
  • 檔案(名稱含 .)永遠不推入 — 只用 parentLen + name.length() 更新答案。
java
// java
// LC 388 - Longest Absolute File Path
// IDEA: Stack indexed by nesting depth — stack.peek() = length of the current
//       directory prefix (with trailing '/'); pop until size == depth to leave sibling branches
// time = O(N), space = O(D)  // N = input length, D = max depth
public int lengthLongestPath(String input) {
    Deque<Integer> stack = new ArrayDeque<>(); // prefix length per depth
    int maxLen = 0;
    for (String line : input.split("\n")) {
        int depth = line.lastIndexOf('\t') + 1;  // tabs are leading & contiguous
        String name = line.substring(depth);
        while (stack.size() > depth) stack.pop();          // leave finished branches
        int parentLen = stack.isEmpty() ? 0 : stack.peek();
        int curLen = parentLen + name.length();
        if (name.indexOf('.') >= 0) {
            maxLen = Math.max(maxLen, curLen);             // file → candidate answer
        } else {
            stack.push(curLen + 1);                        // dir → +1 for '/'
        }
    }
    return maxLen;
}
python
# python
# LC 388 - Longest Absolute File Path
# IDEA: stack[d] = length of the directory prefix at depth d (trailing '/' counted);
#       pop until len(stack) == depth so sibling branches are discarded
# time = O(N), space = O(D)
def lengthLongestPath(input: str) -> int:
    stack = []          # prefix length per depth
    best = 0
    for line in input.split('\n'):
        depth = len(line) - len(line.lstrip('\t'))
        name = line[depth:]
        while len(stack) > depth:
            stack.pop()
        parent = stack[-1] if stack else 0
        cur = parent + len(name)
        if '.' in name:
            best = max(best, cur)      # file
        else:
            stack.append(cur + 1)      # dir, +1 for '/'
    return best

陷阱

  • 答案是最長的到檔案的路徑,所以碰到目錄時絕對不要更新最大值。
  • 不要切掉字串之後才用 line.count('\t') 算深度 — 深度只能來自開頭的 tab。
  • 空輸入/完全沒有檔案 → 回傳 0

2-14) Longest Valid Parentheses (LC 32) — 索引堆疊搭配基準哨兵 Priority 5 of 5 — Must know — expect it in almost every loop

模板 8:索引堆疊 + 哨兵基準。 不要存字元,改存索引,並且先塞一個 -1 當作「目前這段合法區塊前一格的索引」。遇到 ) 彈出之後,新的堆疊頂端就是最後一個沒配對到的索引,所以 i - stack.peek() 就是以 i 結尾的合法長度 — 完全不用另外記長度。

遇到 ) 的兩種情況

text
pop, then:
  stack empty  → this ')' is unmatched → push i as the NEW base
  stack !empty → length = i - stack.top()

s = ")()())" 上的圖解追蹤

text
i=0 ')'  pop -1 → empty → push 0        stack=[0]        best=0
i=1 '('  push 1                          stack=[0,1]
i=2 ')'  pop 1 → top=0 → 2-0 = 2         stack=[0]        best=2
i=3 '('  push 3                          stack=[0,3]
i=4 ')'  pop 3 → top=0 → 4-0 = 4         stack=[0]        best=4
i=5 ')'  pop 0 → empty → push 5          stack=[5]        best=4  ✓
java
// java
// LC 32 - Longest Valid Parentheses
// IDEA: Stack of indices seeded with -1 (base). On ')' pop; if empty this ')' becomes
//       the new base, else answer candidate = i - stack.peek()
// time = O(N), space = O(N)
public int longestValidParentheses(String s) {
    Deque<Integer> stack = new ArrayDeque<>();
    stack.push(-1);                 // base = index before the current valid block
    int best = 0;
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == '(') {
            stack.push(i);
        } else {
            stack.pop();
            if (stack.isEmpty()) stack.push(i);                 // unmatched ')' → new base
            else best = Math.max(best, i - stack.peek());
        }
    }
    return best;
}
python
# python
# LC 32 - Longest Valid Parentheses
# IDEA: index stack with -1 sentinel; i - stack[-1] = length of valid run ending at i
# time = O(N), space = O(N)
def longestValidParentheses(s: str) -> int:
    stack = [-1]          # base index
    best = 0
    for i, c in enumerate(s):
        if c == '(':
            stack.append(i)
        else:
            stack.pop()
            if not stack:
                stack.append(i)               # new base
            else:
                best = max(best, i - stack[-1])
    return best

陷阱

  • 忘了先塞 -1,所有從索引 0 開始的區段都會算錯。
  • 堆疊裡存的是索引,絕不是字元 — 整個技巧就靠索引運算。
  • O(1) 空間的替代解:走兩趟(左→右,再右→左),用 openclose 計數,當 close > open(反向時是 open > close)就歸零。

2-15) Maximum Binary Tree (LC 654) — 用單調遞減堆疊建出笛卡兒樹 Priority 4 of 5 — High value — a gap here costs you rounds

模板 9:用單調堆疊建樹。 直覺的「找最大值,再左右遞迴」是 O(n²)。用遞減堆疊可以一趟建出同一棵樹:被 num 彈掉的元素都比 num 小、而且都在它左邊 → 它們變成 num子樹;活下來的堆疊頂端比 num 大 → num 變成它的子節點。根就是堆疊最底部那個。

text
nums = [3,2,1,6,0,5]

3 → stack[3]
2 → 3>2, 3.right = 2            stack[3,2]
1 → 2>1, 2.right = 1            stack[3,2,1]
6 → pop 1,2,3 (each becomes 6.left in turn, last popped wins) → stack empty
                                 stack[6]      root = 6
0 → 6.right = 0                 stack[6,0]
5 → pop 0 → 5.left = 0; top 6 → 6.right = 5   stack[6,5]
java
// java
// LC 654 - Maximum Binary Tree
// IDEA: Monotonic DECREASING stack of nodes. Nodes popped by num become num's left
//       subtree (last popped = direct left child); surviving top adopts num as right child
// time = O(N), space = O(N)   // beats the O(N^2) divide & conquer build
public TreeNode constructMaximumBinaryTree(int[] nums) {
    Deque<TreeNode> stack = new ArrayDeque<>(); // values decreasing: bottom -> top
    for (int num : nums) {
        TreeNode cur = new TreeNode(num);
        while (!stack.isEmpty() && stack.peek().val < num) {
            cur.left = stack.pop();          // last popped ends up as the left child
        }
        if (!stack.isEmpty()) stack.peek().right = cur;
        stack.push(cur);
    }
    return stack.isEmpty() ? null : stack.peekLast(); // bottom of stack = global max = root
}
python
# python
# LC 654 - Maximum Binary Tree
# IDEA: monotonic decreasing stack of nodes; popped nodes chain into cur.left,
#       remaining top takes cur as its right child; stack[0] is the root
# time = O(N), space = O(N)
def constructMaximumBinaryTree(nums):
    stack = []                      # node values decreasing
    for num in nums:
        cur = TreeNode(num)
        while stack and stack[-1].val < num:
            cur.left = stack.pop()  # overwritten each pop -> keeps the LAST popped
        if stack:
            stack[-1].right = cur
        stack.append(cur)
    return stack[0] if stack else None

為什麼 cur.left 會被一直覆寫: 每彈出一次就重新指定一次 cur.left,而被彈出的節點彼此早就串好了(先被彈出的是前一個節點的右子節點),所以迴圈結束後 cur.left 正好指向整個被彈出區塊的根。

相關: LC 1008(Construct BST from Preorder Traversal)用的是鏡像的想法 — 遞增堆疊,較大的值成為最後一個被彈出節點的右子節點。

2-16) Min Stack (LC 155) — 輔助的非遞增堆疊 Priority 4 of 5 — High value — a gap here costs you rounds

模板 10:平行的「最小值堆疊」。 另外維護一個值非遞增的堆疊;它的頂端永遠是目前存活元素中的最小值。這就是單調堆疊在設計題裡的樣子。

java
// java
// LC 155 - Min Stack
// IDEA: second stack keeps a non-increasing sequence of minima; push a new min when
//       val <= current min (the '=' is REQUIRED so duplicates survive matching pops)
// time = O(1) per op, space = O(N)
class MinStack {
    private final Deque<Integer> stack = new ArrayDeque<>();
    private final Deque<Integer> mins  = new ArrayDeque<>(); // non-increasing

    public void push(int val) {
        stack.push(val);
        if (mins.isEmpty() || val <= mins.peek()) mins.push(val);
    }
    public void pop() {
        int v = stack.pop();
        if (v == mins.peek()) mins.pop();
    }
    public int top()    { return stack.peek(); }
    public int getMin() { return mins.peek(); }
}
python
# python
# LC 155 - Min Stack
# IDEA: auxiliary non-increasing stack of minima; '<=' on push keeps duplicate minima
# time = O(1) per op, space = O(N)
class MinStack:
    def __init__(self):
        self.stack = []
        self.mins  = []          # non-increasing

    def push(self, val: int) -> None:
        self.stack.append(val)
        if not self.mins or val <= self.mins[-1]:
            self.mins.append(val)

    def pop(self) -> None:
        if self.stack.pop() == self.mins[-1]:
            self.mins.pop()

    def top(self) -> int:
        return self.stack[-1]

    def getMin(self) -> int:
        return self.mins[-1]

經典 bug: 只在 val < mins.peek()(嚴格)時才推入新的最小值。碰到 push(0); push(0); pop();,那唯一存下的 0 會被移掉,getMin() 就回傳錯的值。要用 <=省空間的變形: 在單一堆疊裡存 (val, minSoFar) 這種 pair — 操作一樣是 O(1),而且面試壓力下比較好講清楚。

2-17) 既有模板的各種變形

LC # 題目 基礎模板 變化點
1475 Final Prices With a Special Discount in a Shop 模板 2(next smaller) 是 next smaller 或相等 — 條件改成 prices[stack[-1]] >= prices[i] 才彈出,而且折扣是 price - prices[i],不是索引距離
1019 Next Greater Node In Linked List 模板 1(next greater) 一樣的遞減堆疊,但輸入是鏈結串列 — 先走一趟轉成陣列(或邊走邊推入 (index, val)),因為答案陣列需要隨機存取
768 Max Chunks To Make Sorted II 模板 1(遞減彈出) 堆疊裡放的是各區塊的最大值,不是原始元素;答案 = 最後的堆疊大小
769 Max Chunks To Make Sorted 模板 1(退化版) 值是 0..n-1 的排列,所以用一個 running max 就能取代堆疊:只要 runningMax == i 就切一塊
1047 / 1209 Remove All Adjacent Duplicates In String (I / II) 模板 5(堆疊帶資訊) 堆疊存 (char, count)countk 就彈出 — LC 1047 就是 k = 2 的特例

Max Chunks To Make Sorted II (LC 768) — 區塊最大值堆疊

java
// java
// LC 768 - Max Chunks To Make Sorted II
// IDEA: monotonic increasing stack of chunk MAXIMA. A value smaller than the top must
//       merge every chunk it is smaller than; the merged chunk keeps the largest max
// time = O(N), space = O(N)
public int maxChunksToSorted(int[] arr) {
    Deque<Integer> stack = new ArrayDeque<>(); // chunk maxima, increasing bottom -> top
    for (int num : arr) {
        if (!stack.isEmpty() && num < stack.peek()) {
            int maxOfMerged = stack.pop();
            while (!stack.isEmpty() && num < stack.peek()) stack.pop();
            stack.push(maxOfMerged);           // merged chunk keeps the old max
        } else {
            stack.push(num);                   // starts a new chunk
        }
    }
    return stack.size();
}
python
# python
# LC 768 - Max Chunks To Make Sorted II
# IDEA: increasing stack of chunk maxima; merging keeps the largest max
# time = O(N), space = O(N)
def maxChunksToSorted(arr):
    stack = []                       # chunk maxima, increasing
    for num in arr:
        if stack and num < stack[-1]:
            merged_max = stack.pop()
            while stack and num < stack[-1]:
                stack.pop()
            stack.append(merged_max)
        else:
            stack.append(num)
    return len(stack)

# LC 769 - Max Chunks To Make Sorted (values are a permutation of 0..n-1)
# time = O(N), space = O(1)
def maxChunksToSortedI(arr):
    chunks, running_max = 0, -1
    for i, num in enumerate(arr):
        running_max = max(running_max, num)
        if running_max == i:         # prefix holds exactly the values 0..i
            chunks += 1
    return chunks

2-18) Steps to Make Array Non-decreasing (LC 2289) — 把 DP 值扛在單調堆疊上 Priority 4 of 5 — High value — a gap here costs you rounds

leetcode_python/Stack/steps-to-make-array-non-decreasing.py

模板 11:堆疊扛的是 dp 值,不只是位置。 上面每個模板都在彈出的那一刻算出一個最終數字(寬度、面積、距離)。這題不一樣:被彈出的元素會把自己的 dp 值往上交給彈出它的那個元素,答案因此一路串起來:dp[i] = max(我彈掉的所有人的 dp) + 1。答案就是 max(dp),也就是最長的那條鏈。

核心想法

一輪會同時刪掉所有滿足 nums[i-1] > nums[i]nums[i]。逐輪模擬是 O(n²)(n 最大到 1e5),所以換個角度,對每個元素問:

text
dp[i] = the round in which nums[i] is deleted     (0 = never deleted)
answer = max(dp)

nums[i] 最終會被它左邊最近、而且嚴格大於它的元素刪掉 — 但那個「兇手」要等到兩者之間的元素全部消失,才碰得到 nums[i]。而這段等待時間,正好就是堆疊交接下來的東西:

text
keep a DECREASING stack of indices, then for each i:

  while nums[i] >= nums[stack[-1]]:        # this top can never kill me
      cur = max(cur, dp[stack.pop()])      #   -> inherit its deadline

  if stack:      dp[i] = cur + 1           # top is > nums[i]: my killer.
                                           #   it reaches me one round after
                                           #   the last of the popped ones died
  else:          dp[i] = 0                 # nothing bigger on the left -> I survive

用「吃掉」這個比喻看,有兩件事會變得很明顯:

  • 被彈掉的,就是我活得比它久的。 任何 <= nums[i] 的元素,死期都不會晚於 nums[i],所以它的死期是我的死期的下界。
  • +1,不是 + 被彈掉的個數 同一輪的刪除是同時發生的,所以一整批被彈掉的元素可能在同一輪就消失;只有最慢的那個(取 max,不是 sum)會拖住我的兇手,而且只拖剛好一輪。

讀懂堆疊:steps_to_remove 是什麼意思

把堆疊想成「值/死期」的配對,會比想成索引好記得多:

text
stack = [ [val, steps_to_remove], ... ]

val             = the element's value
steps_to_remove = WHICH ROUND this element is deleted in   (0 = never)

第二個數字不是工作量、不是距離、也不是嵌套深度 — 它是日曆上的輪數。照著這個階梯讀:

steps_to_remove 意思 為什麼
0 永遠不會被刪 左邊沒有任何嚴格比它大的元素(它是前綴最大值),所以沒有人刪得掉它
1 第 1 輪被刪 它的左鄰居本來就嚴格比它大 — 規則立刻觸發,不需要任何人先讓路
2 第 2 輪被刪 它和兇手之間那一段需要 1 輪才會消失;第 2 輪開始時它才被暴露出來
3 第 3 輪被刪 那一段需要 2 輪;之後兇手才會變成它的左鄰居
k k 輪被刪 它前面那一段要 k - 1 輪才清得掉,下一輪才輪到它

所以 steps_to_remove = k 永遠是在說*「我前面還有 k - 1 輪,然後才換我」* — 這正是遞推式為什麼是 max(被彈掉的) + 1,也是為什麼答案要對所有死期取 max:最後一個排定的刪除發生完,陣列就是非遞減的了。

有一個結論值得講清楚,因為它是最常見的 off-by-one:死期是 0 的元素不是「在第 0 輪被刪掉」的元素,而是根本沒有死期的元素 — 這也是為什麼程式在 堆疊為空 的那個分支指派 0,而且從不把它算進 res

圖解追蹤 — nums = [5,3,4,4,7]

text
i  nums[i]  pops (dp inherited)          stack after       dp[i]                 res
0    5      -                            [0]               0   (nothing bigger)   0
1    3      none (3 < 5)                 [0,1]             0+1 = 1                1
2    4      pop 1 -> cur=max(0,dp1)=1    [0,2]             1+1 = 2                2
3    4      pop 2 -> cur=max(0,dp2)=2    [0,3]             2+1 = 3                3
4    7      pop 3 (cur=3), pop 0         [4]               0   (stack emptied)     3

dp = [0,1,2,3,0]  ->  answer 3
check: round1 deletes 3 -> [5,4,4,7]; round2 deletes the first 4 -> [5,4,7];
       round3 deletes the second 4 -> [5,7]  ✓

逐步演練 — 完整的 LC 範例,(val, steps) 配對堆疊

nums = [5,3,4,4,7,3,6,11,8,5,11],答案應該是 3。同一個演算法,改用配對寫法,讓死期直接顯示在堆疊上。只要 stack[-1][0] <= val 就彈出,繼承 max(被彈掉的 steps),堆疊還有東西就 +1(空了就是 0):

# val 彈出前的堆疊 彈出 steps 彈出後的堆疊 res
0 5 [] 0 — 堆疊為空,前綴最大值,永遠不死 [(5,0)] 0
1 3 [(5,0)] 0 + 1 = 1 — 已經是 5 > 3,下一輪就死 [(5,0),(3,1)] 1
2 4 [(5,0),(3,1)] (3,1) 1 + 1 = 2 — 得先活過那個 3,然後才輪到 5 收它 [(5,0),(4,2)] 2
3 4 [(5,0),(4,2)] (4,2) 2 + 1 = 3 — 因為 <= 被彈出;繼承死期 2 [(5,0),(4,3)] 3
4 7 [(5,0),(4,3)] (4,3)(5,0) 0 — 堆疊被清空,新的前綴最大值 [(7,0)] 3
5 3 [(7,0)] 0 + 1 = 1 [(7,0),(3,1)] 3
6 6 [(7,0),(3,1)] (3,1) 1 + 1 = 2 [(7,0),(6,2)] 3
7 11 [(7,0),(6,2)] (6,2)(7,0) 0 — 又被清空 [(11,0)] 3
8 8 [(11,0)] 0 + 1 = 1 [(11,0),(8,1)] 3
9 5 [(11,0),(8,1)] 0 + 1 = 1 — 已經是 8 > 5;那個 8 對這個 5 的死期毫無影響 [(11,0),(8,1),(5,1)] 3
10 11 [(11,0),(8,1),(5,1)] (5,1)(8,1)(11,0) 0 [(11,0)] 3

結果 max(steps) = 3

現在把這些死期當成一張時程表讀回去,和題目敘述印出來的那幾輪對照:

text
idx     0  1  2  3  4  5  6  7  8  9 10
nums    5  3  4  4  7  3  6 11  8  5 11
steps   0  1  2  3  0  1  2  0  1  1  0
                                          <- every nonzero entry is a scheduled funeral

round 1  deletes steps==1  -> idx 1,5,8,9  (3, 3, 8, 5)
         [5,3,4,4,7,3,6,11,8,5,11] -> [5,4,4,7,6,11,11]

round 2  deletes steps==2  -> idx 2,6      (the first 4, and 6)
         [5,4,4,7,6,11,11] -> [5,4,7,11,11]

round 3  deletes steps==3  -> idx 3        (the second 4)
         [5,4,7,11,11] -> [5,7,11,11]      non-decreasing, stop

3 rounds  ==  max(steps)   ✓

有兩列值得停下來看,因為直覺通常就斷在這裡:

  • 第 3 列(第二個 4)。 它是因為 <= 被彈掉的,不是 <。即使左邊就有一個 5,它自己的死期也不是 1,因為第一個 4 會一直擋在中間直到第 2 輪 — 所以是 3。這一列就是彈出條件寫成嚴格 > 會算錯的那一列,也是整個答案的來源。
  • 第 9 列(8 後面的那個 5)。 堆疊上還活著四個元素,但這個 5 在第 1 輪就死了:它的左鄰居 8 本來就比它大,不需要先清掉任何東西。堆疊很深不代表死期很晚 — 死期只取決於元素和兇手之間卡著什麼。

而第 4、7、10 列展示了這個不變式的另一半:每次堆疊被清空,新進來的元素就是一個前綴最大值,死期為 0,而它後面所有的死期都已經記進 res 了 — 陣列實際上是從這個元素重新開始的。

>=> 的陷阱

刪除規則只在 nums[i-1] > nums[i] 時觸發,所以相等的元素永遠不會被刪掉 — 這也代表它不可能是兇手,必須和比較小的元素一樣被彈出。把彈出條件寫成嚴格的 >,就是這題專門要抓的 bug:

text
nums = [5,3,4,4,7]
pop on >=  ->  dp = [0,1,2,3,0]  ->  3   ✓
pop on >   ->  dp = [0,1,2,1,0]  ->  2   ✗   (idx 3 wrongly adopts idx 2 as its killer)

掃描方向會翻轉這個運算子,這點值得記住:

掃描方向 彈出條件 彈出的是什麼
由左往右 nums[i] >= nums[stack[-1]]非嚴格 殺不死我的元素 — 比較小的和相等的
由右往左 nums[i] > nums[stack[-1]]嚴格 我吃掉的元素 — 只有嚴格比較小的

模式(Python)

python
# python
# LC 2289 - Steps to Make Array Non-decreasing
# IDEA: monotonic DECREASING stack of indices; dp[i] = round in which nums[i] dies.
#       A popped element hands its dp up: dp[i] = max(popped dp) + 1
# time = O(n), space = O(n)
def totalSteps(nums):
    n = len(nums)
    dp = [0] * n          # dp[i] = round nums[i] is removed (0 = never)
    stack = []            # indices, values monotonically DECREASING
    res = 0

    for i in range(n):
        cur = 0
        # NOTE !!! `>=` — an equal element is never deleted, so it is not a killer
        while stack and nums[i] >= nums[stack[-1]]:
            cur = max(cur, dp[stack.pop()])
        if stack:                       # a strictly greater element on the left = my killer
            dp[i] = cur + 1
            res = max(res, dp[i])
        stack.append(i)                 # else dp[i] stays 0: never removed

    return res

同一套 dp 由右往左掃 — 每個元素從自己吃掉的元素算出答案,於是「我左邊有沒有更大的元素?」這個分支就消失了:

python
# python
# LC 2289 - variant: right -> left, steps = max(steps + 1, dp[j])
# time = O(n), space = O(n)
def totalSteps_rtl(nums):
    n = len(nums)
    dp = [0] * n
    stack, res = [], 0
    for i in range(n - 1, -1, -1):
        steps = 0
        while stack and nums[i] > nums[stack[-1]]:   # strict: only what I eat
            # one more round than eaten so far, but j may itself be dying until dp[j]
            steps = max(steps + 1, dp[stack.pop()])
        dp[i] = steps
        res = max(res, steps)
        stack.append(i)
    return res

模式(Java)

java
// java
// LC 2289 - Steps to Make Array Non-decreasing
// IDEA: decreasing stack of indices; dp[i] = round nums[i] is removed.
//       dp[i] = max(dp of popped) + 1 when a strictly greater element remains on the left
// time = O(N), space = O(N)
public int totalSteps(int[] nums) {
    int n = nums.length, res = 0;
    int[] dp = new int[n];                          // dp[i] = round nums[i] dies (0 = never)
    Deque<Integer> stack = new ArrayDeque<>();      // indices, values decreasing

    for (int i = 0; i < n; i++) {
        int cur = 0;
        while (!stack.isEmpty() && nums[i] >= nums[stack.peek()]) {
            cur = Math.max(cur, dp[stack.pop()]);   // inherit the deadline
        }
        if (!stack.isEmpty()) {
            dp[i] = cur + 1;
            res = Math.max(res, dp[i]);
        }
        stack.push(i);
    }
    return res;
}

改用 (值, 步數) 的配對堆疊完全等價,還可以省掉 dp 陣列 — 推入 (num, cur) 而不是索引,彈出條件改成 stack[-1][0] <= num上面逐步演練用的就是這種寫法。當索引沒有別的用途時就用它。

相似題目

題目 LC # 關鍵差異
Steps to Make Array Non-decreasing 2289 被彈出元素的 dp 會被繼承dp[i] = max(被彈掉的 dp) + 1
Online Stock Span 901 同樣是「把一個值透過彈出一路帶上來」,但彙總方式是把彈掉的 span 相加,不是 max+1
Car Fleet II 1776 由右往左的堆疊,每台車的碰撞時間由被彈掉的車推導而來 — 一樣的串接,只是 dp 是實數(§2-10
Car Fleet 853 要的是存活者的數量,不是誰在第幾輪死掉 — 所以它的堆疊從來不彈出,兩個變數就夠了(§2-10
Count Robot Groups 4045 和 853 同一套貪婪,但沒有終點,所以沒有抵達時間可比 — 改比速度(§2-10
Asteroid Collision 735 同樣是「大的吃掉小的」模擬;答案是存活者,所以不需要帶 dp
Minimum Cost Tree From Leaf Values 1130 一樣是比較小就彈出,但每次彈出彙總的是成本,不是輪數
Largest Rectangle in Histogram 84 當作對照:那裡的彈出算出的是最終值(面積),沒有人會去繼承它

2-19) 值得知道的經典堆疊題(非單調)

這些用的是普通堆疊(沒有單調不變式),但常常和上面那些模式一起出現。

題目 LC # 關鍵技巧 難度
Simplify Path 71 / 切開;元件推入,.. 彈出,./空字串跳過 Medium
Backspace String Compare 844 每個字串一個堆疊,或從後往前用雙指標做到 O(1) 空間 Easy
Remove All Adjacent Duplicates In String 1047 推入字元,和頂端相同就彈出 Easy
Remove All Adjacent Duplicates in String II 1209 堆疊存 (char, count),count 到 k 就彈出 Medium
Flatten Nested List Iterator 341 堆疊裡放 iterator/list;在 hasNext() 中惰性攤平 Medium
Binary Search Tree Iterator 173 受控的迭代中序 — 堆疊存左脊 Medium
Maximum Frequency Stack 895 freq map + 從頻率對應到值堆疊的 map Hard
Baseball Game 682 直接用堆疊模擬 +DC Easy