單調堆疊資料結構
範圍 — 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 | 遞增 | 找比目前元素小的元素 |
| 直方圖面積 | 矩形/面積問題 | 遞增 | 用高度算面積 |
| 環狀陣列 | 循環問題 | 視情況 | 處理環狀序列 |
| 樣式驗證 | 序列驗證 | 視情況 | 驗證特定樣式 |
| 最佳化堆疊 | 最大/最小問題 | 視情況 | 維持最佳候選 |
通用模板
def monotonic_stack_template(arr):
"""
Universal template for monotonic stack problems
Modify the condition and processing logic based on problem requirements
"""
stack = [] # Store indices or values
result = []
for i, val in enumerate(arr):
# Pop elements that violate monotonic property
while stack and should_pop(stack, val, i):
# Process the popped element
popped = stack.pop()
process_popped_element(popped, i, result)
# Add current element to stack
stack.append(i) # or val depending on problem
# Process remaining elements in stack
while stack:
popped = stack.pop()
process_remaining_element(popped, result)
return result
def should_pop(stack, current_val, current_idx):
"""Define when to pop based on problem requirements"""
# For next greater: return arr[stack[-1]] <= current_val
# For next smaller: return arr[stack[-1]] >= current_val
pass
def process_popped_element(popped_idx, current_idx, result):
"""Process element when it's popped (found its next greater/smaller)"""
pass
def process_remaining_element(popped_idx, result):
"""Process elements remaining in stack at the end"""
pass
// Java Universal Template
public int[] monotonicStackTemplate(int[] arr) {
Stack<Integer> stack = new Stack<>();
int[] result = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
// Pop elements that violate monotonic property
while (!stack.isEmpty() && shouldPop(stack, arr, i)) {
int poppedIdx = stack.pop();
processElement(poppedIdx, i, result, arr);
}
// Add current element to stack
stack.push(i);
}
// Process remaining elements
while (!stack.isEmpty()) {
int poppedIdx = stack.pop();
processRemainingElement(poppedIdx, result);
}
return result;
}
private boolean shouldPop(Stack<Integer> stack, int[] arr, int currentIdx) {
// Define condition based on problem requirements
return arr[stack.peek()] <= arr[currentIdx]; // For next greater
}
模板 1:Next Greater Element(遞減堆疊) — LC 496
def next_greater_element(nums):
"""
Find next greater element for each element
LC 496, LC 503, LC 739
"""
n = len(nums)
result = [-1] * n
stack = [] # Store indices
for i in range(n):
# Pop smaller or equal elements
while stack and nums[stack[-1]] < nums[i]:
idx = stack.pop()
result[idx] = nums[i] # Found next greater
stack.append(i)
return result
// Java Template 1
public int[] nextGreaterElement(int[] nums) {
int n = nums.length;
int[] result = new int[n];
Arrays.fill(result, -1);
Stack<Integer> stack = new Stack<>();
for (int i = 0; i < n; i++) {
while (!stack.isEmpty() && nums[stack.peek()] < nums[i]) {
result[stack.pop()] = nums[i];
}
stack.push(i);
}
return result;
}
模板 2:Next Smaller Element(遞增堆疊) — LC 84
def next_smaller_element(nums):
"""
Find next smaller element for each element
Used in LC 84, LC 42
"""
n = len(nums)
result = [-1] * n
stack = [] # Store indices
for i in range(n):
# Pop greater or equal elements
while stack and nums[stack[-1]] > nums[i]:
idx = stack.pop()
result[idx] = nums[i] # Found next smaller
stack.append(i)
return result
模板 3:Largest Rectangle in Histogram — LC 84
def largest_rectangle_area(heights):
"""
Find largest rectangle area in histogram
LC 84, LC 85
"""
stack = [] # Store indices
max_area = 0
heights.append(0) # Add sentinel
for i, h in enumerate(heights):
# Pop taller bars and calculate area
while stack and heights[stack[-1]] > h:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
return max_area
// Java Template 3
public int largestRectangleArea(int[] heights) {
Stack<Integer> stack = new Stack<>();
int maxArea = 0;
int n = heights.length;
for (int i = 0; i <= n; i++) {
int h = (i == n) ? 0 : heights[i];
while (!stack.isEmpty() && heights[stack.peek()] > h) {
int height = heights[stack.pop()];
int width = stack.isEmpty() ? i : i - stack.peek() - 1;
maxArea = Math.max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
模板 4:環狀陣列處理 — LC 503
def next_greater_circular(nums):
"""
Find next greater element in circular array
LC 503
"""
n = len(nums)
result = [-1] * n
stack = []
# Process array twice to handle circular nature
for i in range(2 * n):
# Pop smaller elements
while stack and nums[stack[-1]] < nums[i % n]:
idx = stack.pop()
result[idx] = nums[i % n]
# Only add indices from first pass
if i < n:
stack.append(i)
return result
模板 5:堆疊中帶額外資訊
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
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 題
模式選擇策略
決策流程圖
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
逐步分析題目
-
先看清核心需求
- 查詢 next/previous 元素 → 模板 1、2、4
- 計算面積/矩形 → 模板 3
- 樣式驗證 → 模板 5、6
- 最佳化問題 → 模板 1、2、5
-
決定堆疊順序
- 要找較大的元素 → 遞減堆疊(彈出較小的)
- 要找較小的元素 → 遞增堆疊(彈出較大的)
- 面積計算 → 通常是遞增堆疊
- 樣式偵測 → 視樣式而定
-
決定處理方向
- 由左往右:最常見,也最自然
- 由右往左:找「next」元素時有時比較好寫
- 環狀:把陣列處理多遍
-
決定堆疊裡放什麼
- 索引:需要位置資訊時
- 值:只需要比大小時
- 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 模式
# 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
子陣列的貢獻法
# 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)
直方圖面積計算
# 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:判斷模式類型
- 找關鍵字:next/previous、greater/smaller、面積、矩形
- 確認有沒有環狀/循環的需求
- 判斷是否需要驗證或樣式偵測
-
步驟 2:挑對模板
- 用決策流程圖
- 想清楚堆疊順序(遞增還是遞減)
- 決定堆疊裡要存什麼資訊
-
步驟 3:實作核心邏輯
- 準備好堆疊與結果容器
- 寫出 while 迴圈,彈出條件要對
- 妥善處理彈出的元素
- 處理最後留在堆疊裡的元素
-
步驟 4:處理邊界情況
- 空陣列
- 只有一個元素
- 所有元素都相同
- 嚴格遞增/遞減的序列
-
步驟 5:最佳化與驗證
- 確認時間複雜度是 O(n)
- 檢查空間複雜度
- 用範例輸入驗證
- 必要時處理整數溢位
常見錯誤與提醒
常見錯誤
- 堆疊順序搞反:next greater 題卻用遞增堆疊(應該用遞減)
- 索引與值混淆:需要算距離時卻只存了值
- 處理不完整:忘了處理最後留在堆疊裡的元素
- 邊界問題:沒有妥善處理堆疊為空的情況
- 環狀邏輯:環狀陣列處理不正確(漏了第二趟)
- 條件寫錯:比較運算子用錯(< vs <=、> vs >=)
最佳實務
- 需要位置資訊時,一律存索引
- 用哨兵值(例如 0)簡化邊界處理
- 除非真的需要由右往左,否則由左往右處理
- 變數名要清楚:用
stack、result、current_idx,不要用s、res、i - 在 while 條件加註解,說明維持的單調性質
- 先處理邊界情況,再寫主要演算法
面試技巧
-
辨識模式
- 聽到「next greater/smaller」這類關鍵字就要有反應
- 面積/矩形題常常用得上單調堆疊
- 序列驗證題可能需要以堆疊為基礎的做法
-
解題流程
- 先從暴力解開始,把題目搞懂
- 判斷單調性質能不能把解法優化
- 畫例子,把堆疊的行為視覺化
-
面試時的溝通
- 解釋為什麼這題適合用單調堆疊
- 用例子把堆疊的狀態走一遍給面試官看
- 討論時間/空間複雜度的取捨
-
實作提醒
- 從模板骨架開始寫
- 把心力放在 while 條件寫對
- 用簡單的例子測試(像 [2,1,2,4,3,1])
-
預期會有的追問
- 有重複值怎麼辦?
- 如果要的是 previous 而不是 next 呢?
- 空間複雜度還能再優化嗎?
- 怎麼延伸到二維問題?
相關主題
- 堆疊:單調堆疊是堆疊這個資料結構的特化應用
- 雙端佇列:滑動視窗最大值用的是單調雙端佇列
- 雙指標:某些面積計算題的替代解法
- 動態規劃:有些最佳化問題會把 DP 和單調堆疊搭在一起
- 二分搜尋:在有序結構中找邊界
- 線段樹:區間最大/最小值的進階查詢
LC 範例
2-1) Daily Temperatures (LC 739) — 單調遞減堆疊
堆疊存索引;遇到更暖的一天就彈出。
// 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) — 單調遞增堆疊
來了比較矮的柱子就彈出;用堆疊算出寬度後求面積。
// 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 的查詢。
// 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) — 單調堆疊
來了比較高的柱子就彈出;接到的水 =(較小高度的差)* 寬度。
// 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 查詢。
// 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 = 距離上一個更高價過了幾天。
// 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) — 單調堆疊
對每個元素,找出它是最小值的左右邊界;用單調堆疊做。
// 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,這個數量會拆成兩個互相獨立的選擇:
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
# 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] 上的圖解追蹤:
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) — 單調遞增堆疊
維持遞增堆疊;來了比較小的數字就把前面的移掉。
// 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 的最大矩形邏輯。
// 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.py、leetcode_python/Stack/count-robot-groups.py
這一節講的是反面教材。 LC 853 一向被當成堆疊題教,這個 repo 的 README 也把它標成
**stack**— 但它的堆疊從來不彈出,所以它其實是穿著堆疊外衣的兩變數貪婪。這三題裡只有 LC 1776 真的需要堆疊。能說出是哪一題、以及為什麼,才是這節真正的價值。
三題共通的性質:前方說話算話
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,所以每台車都可以塌縮成一個數字 — 假設前方完全沒人擋路時,它抵達終點的時間:
t_i = (target - position[i]) / speed[i]
後車會併入前面的車隊,條件是它抵達的時間不會更晚,也就是 t_i <= max(前方所有人的 t)。於是由前往後排序,數「新的最大值出現幾次」就好:
# 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
# LC 853 - the same algorithm, stack removed
# time = O(n log n) (the sort), space = O(1)
def carFleet(target, position, speed):
cnt, mx = 0, 0.0
for p, s in sorted(zip(position, speed), reverse=True): # front -> back
t = (target - p) / s
if t > mx: # arrives later than anyone ahead -> its own fleet
cnt += 1
mx = t
return cnt
// java
// LC 853 - Car Fleet, running-max greedy
// time = O(N log N), space = O(N) for the index array (O(1) beyond the sort)
public int carFleet(int target, int[] position, int[] speed) {
int n = position.length;
Integer[] idx = new Integer[n];
for (int i = 0; i < n; i++) idx[i] = i;
Arrays.sort(idx, (a, b) -> position[b] - position[a]); // front -> back
int cnt = 0;
double mx = 0.0;
for (int i : idx) {
double t = (double) (target - position[i]) / speed[i];
if (t > mx) { cnt++; mx = t; }
}
return cnt;
}
兩種寫法在隨機測資上結果一致 — 本來就該一致,因為它們是同一個演算法。挑你在壓力下寫得出來的那一種就好;只是不要聲稱那個堆疊有在做事。
LC 4045 Count Robot Groups — 同一套貪婪,只是沒有終點
機器人會一直往前跑,而且多了一個合併門檻 distance。沒有終點就代表不存在抵達時間,所以每個機器人沒有單一數字可以比較 — 狀態是這個群組「最右側機器人」的 (position, speed) 這一對。機器人 i 會被吸收,只要下面任一個條件成立:
position[i+1] - position[i] <= distance -> already touching at t = 0
speed[i] > cur_s -> closing on the front group, and with
infinite time ANY positive closing rate
eventually eats ANY gap
# python
# LC 4045 - Count Robot Groups (position is given sorted, so no sort is needed)
# IDEA: right -> left; `cur_s` is the speed of the frontmost group that survived.
# time = O(n), space = O(1)
def countGroups(position, speed, distance):
n = len(position)
cur_s = speed[n - 1] # the frontmost robot is always a group on its own
cnt = 1
for i in range(n - 2, -1, -1):
# touching test -> the NEIGHBOUR; closing test -> the FRONT group
if position[i + 1] - position[i] <= distance or speed[i] > cur_s:
continue # absorbed
cnt += 1
cur_s = speed[i] # a new frontmost survivor
return cnt
陷阱:這兩個測試用的是不同的參考對象。 合併時採用的是最右側機器人的狀態,所以有人從後面加入時 cur 完全不動 — 但 t = 0 的那些合併是同時發生的,而且是透過鄰居一路串起來的:
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] 被吸收掉:
if car i reaches car j LATER than j's own collision time
-> j is already gone when i gets there
-> i's real target is whatever is further ahead
-> back up to an older candidate <- THIS is a pop
# python
# LC 1776 - Car Fleet II: ans[i] = when car i collides with the car ahead (-1 = never)
# IDEA: right -> left monotonic stack of cars still catchable. Pop a candidate that
# cannot be caught, or that dies before we reach it.
# time = O(n), space = O(n)
def getCollisionTimes(cars):
n = len(cars)
ans = [-1.0] * n
st = [] # indices, front -> back
for i in range(n - 1, -1, -1):
p, s = cars[i]
while st:
j = st[-1]
pj, sj = cars[j]
# (a) not faster than j -> can never reach it
# (b) reaching j takes longer than j survives -> aim further ahead
if s <= sj or (ans[j] > 0 and (pj - p) / (s - sj) >= ans[j]):
st.pop()
else:
break
if st:
j = st[-1]
ans[i] = (cars[j][0] - p) / (s - cars[j][1])
st.append(i)
return ans
# cars = [[1,2],[2,1],[4,3],[7,2]] -> [1.0, -1.0, 3.0, -1.0]
# cars = [[3,4],[5,4],[6,3],[9,1]] -> [2.0, 1.0, 1.5, -1.0]
帶進面試房間的判準
| 題目要你給的是 | 前方的行為是 | 該用什麼結構 |
|---|---|---|
| 存活群組的數量(853、4045) | 永久的 — 存活者不會被回頭修改 | 一兩個變數(貪婪) |
| 每個元素各自的時間或值(1776、2289) | 暫時的 — 前方自己也會在已知時間死掉 | 真正的堆疊,而且會彈出 |
所以在面試官面前誠實的講法是:「853 和 4045 是同一套由右往左的貪婪 — LC 853 教科書解法的那個堆疊從來不彈出,所以我會改成維護一個滾動最大值。1776 才是堆疊真正派上用場的地方,因為已經算出來的答案有可能被推翻。」
2-11) Asteroid Collision (LC 735) — 堆疊模擬
向右飛的留在堆疊上;向左飛的一直和頂端相撞,直到穩定。
// 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)。最大值與最小值各跑一趟單調堆疊;每彈出一個元素,就算出它以最大/最小值的身分主宰了多少個子陣列。
核心想法
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] 擔任最大/最小值的子陣列數量:
count = (mid − L) × (R − mid)
contribution = nums[mid] × count
哨兵迴圈讓 i 從 0 跑到 n(含)。當 i == n 時,用 n 當右邊界把堆疊裡剩下的索引全部清掉。
對重複值安全的邊界規則(避免相等元素被重複計算):
- 最大值那趟:
nums[mid] < nums[i]時彈出(嚴格);左邊界是上一個大於或等於的元素。 - 最小值那趟:
nums[mid] > nums[i]時彈出(嚴格);左邊界是上一個小於或等於的元素。
圖解追蹤 — [1, 3, 2] 的最大值那趟
Decreasing stack (max contribution)
i=0: push 0 stack=[0]
i=1: nums[0]=1 < nums[1]=3 → pop mid=0
left=-1, right=1
contrib = 1 * (0-(-1)) * (1-0) = 1*1*1 = 1
push 1 stack=[1]
i=2: nums[1]=3 > nums[2]=2, no pop
push 2 stack=[1,2]
i=3 (sentinel): flush
pop mid=2: left=1, right=3 → 2*(2-1)*(3-2) = 2
pop mid=1: left=-1, right=3 → 3*(1-(-1))*(3-1) = 12
max_sum = 1 + 2 + 12 = 15
min pass (increasing stack) → min_sum = 10
answer = 15 − 10 = 5 ✓
verify: [1]=0,[3]=0,[2]=0,[1,3]=2,[3,2]=1,[1,3,2]=2 → sum = 5
模式(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
// 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,就把剛結束的兄弟分支全部丟掉了。
核心想法
"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
// LC 388 - Longest Absolute File Path
// IDEA: Stack indexed by nesting depth — stack.peek() = length of the current
// directory prefix (with trailing '/'); pop until size == depth to leave sibling branches
// time = O(N), space = O(D) // N = input length, D = max depth
public int lengthLongestPath(String input) {
Deque<Integer> stack = new ArrayDeque<>(); // prefix length per depth
int maxLen = 0;
for (String line : input.split("\n")) {
int depth = line.lastIndexOf('\t') + 1; // tabs are leading & contiguous
String name = line.substring(depth);
while (stack.size() > depth) stack.pop(); // leave finished branches
int parentLen = stack.isEmpty() ? 0 : stack.peek();
int curLen = parentLen + name.length();
if (name.indexOf('.') >= 0) {
maxLen = Math.max(maxLen, curLen); // file → candidate answer
} else {
stack.push(curLen + 1); // dir → +1 for '/'
}
}
return maxLen;
}
# python
# LC 388 - Longest Absolute File Path
# IDEA: stack[d] = length of the directory prefix at depth d (trailing '/' counted);
# pop until len(stack) == depth so sibling branches are discarded
# time = O(N), space = O(D)
def lengthLongestPath(input: str) -> int:
stack = [] # prefix length per depth
best = 0
for line in input.split('\n'):
depth = len(line) - len(line.lstrip('\t'))
name = line[depth:]
while len(stack) > depth:
stack.pop()
parent = stack[-1] if stack else 0
cur = parent + len(name)
if '.' in name:
best = max(best, cur) # file
else:
stack.append(cur + 1) # dir, +1 for '/'
return best
陷阱
- 答案是最長的到檔案的路徑,所以碰到目錄時絕對不要更新最大值。
- 不要切掉字串之後才用
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結尾的合法長度 — 完全不用另外記長度。
遇到 ) 的兩種情況
pop, then:
stack empty → this ')' is unmatched → push i as the NEW base
stack !empty → length = i - stack.top()
在 s = ")()())" 上的圖解追蹤
i=0 ')' pop -1 → empty → push 0 stack=[0] best=0
i=1 '(' push 1 stack=[0,1]
i=2 ')' pop 1 → top=0 → 2-0 = 2 stack=[0] best=2
i=3 '(' push 3 stack=[0,3]
i=4 ')' pop 3 → top=0 → 4-0 = 4 stack=[0] best=4
i=5 ')' pop 0 → empty → push 5 stack=[5] best=4 ✓
// java
// LC 32 - Longest Valid Parentheses
// IDEA: Stack of indices seeded with -1 (base). On ')' pop; if empty this ')' becomes
// the new base, else answer candidate = i - stack.peek()
// time = O(N), space = O(N)
public int longestValidParentheses(String s) {
Deque<Integer> stack = new ArrayDeque<>();
stack.push(-1); // base = index before the current valid block
int best = 0;
for (int i = 0; i < s.length(); i++) {
if (s.charAt(i) == '(') {
stack.push(i);
} else {
stack.pop();
if (stack.isEmpty()) stack.push(i); // unmatched ')' → new base
else best = Math.max(best, i - stack.peek());
}
}
return best;
}
# python
# LC 32 - Longest Valid Parentheses
# IDEA: index stack with -1 sentinel; i - stack[-1] = length of valid run ending at i
# time = O(N), space = O(N)
def longestValidParentheses(s: str) -> int:
stack = [-1] # base index
best = 0
for i, c in enumerate(s):
if c == '(':
stack.append(i)
else:
stack.pop()
if not stack:
stack.append(i) # new base
else:
best = max(best, i - stack[-1])
return best
陷阱
- 忘了先塞
-1,所有從索引0開始的區段都會算錯。 - 堆疊裡存的是索引,絕不是字元 — 整個技巧就靠索引運算。
- O(1) 空間的替代解:走兩趟(左→右,再右→左),用
open/close計數,當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變成它的右子節點。根就是堆疊最底部那個。
nums = [3,2,1,6,0,5]
3 → stack[3]
2 → 3>2, 3.right = 2 stack[3,2]
1 → 2>1, 2.right = 1 stack[3,2,1]
6 → pop 1,2,3 (each becomes 6.left in turn, last popped wins) → stack empty
stack[6] root = 6
0 → 6.right = 0 stack[6,0]
5 → pop 0 → 5.left = 0; top 6 → 6.right = 5 stack[6,5]
// java
// LC 654 - Maximum Binary Tree
// IDEA: Monotonic DECREASING stack of nodes. Nodes popped by num become num's left
// subtree (last popped = direct left child); surviving top adopts num as right child
// time = O(N), space = O(N) // beats the O(N^2) divide & conquer build
public TreeNode constructMaximumBinaryTree(int[] nums) {
Deque<TreeNode> stack = new ArrayDeque<>(); // values decreasing: bottom -> top
for (int num : nums) {
TreeNode cur = new TreeNode(num);
while (!stack.isEmpty() && stack.peek().val < num) {
cur.left = stack.pop(); // last popped ends up as the left child
}
if (!stack.isEmpty()) stack.peek().right = cur;
stack.push(cur);
}
return stack.isEmpty() ? null : stack.peekLast(); // bottom of stack = global max = root
}
# python
# LC 654 - Maximum Binary Tree
# IDEA: monotonic decreasing stack of nodes; popped nodes chain into cur.left,
# remaining top takes cur as its right child; stack[0] is the root
# time = O(N), space = O(N)
def constructMaximumBinaryTree(nums):
stack = [] # node values decreasing
for num in nums:
cur = TreeNode(num)
while stack and stack[-1].val < num:
cur.left = stack.pop() # overwritten each pop -> keeps the LAST popped
if stack:
stack[-1].right = cur
stack.append(cur)
return stack[0] if stack else None
為什麼 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
// LC 155 - Min Stack
// IDEA: second stack keeps a non-increasing sequence of minima; push a new min when
// val <= current min (the '=' is REQUIRED so duplicates survive matching pops)
// time = O(1) per op, space = O(N)
class MinStack {
private final Deque<Integer> stack = new ArrayDeque<>();
private final Deque<Integer> mins = new ArrayDeque<>(); // non-increasing
public void push(int val) {
stack.push(val);
if (mins.isEmpty() || val <= mins.peek()) mins.push(val);
}
public void pop() {
int v = stack.pop();
if (v == mins.peek()) mins.pop();
}
public int top() { return stack.peek(); }
public int getMin() { return mins.peek(); }
}
# python
# LC 155 - Min Stack
# IDEA: auxiliary non-increasing stack of minima; '<=' on push keeps duplicate minima
# time = O(1) per op, space = O(N)
class MinStack:
def __init__(self):
self.stack = []
self.mins = [] # non-increasing
def push(self, val: int) -> None:
self.stack.append(val)
if not self.mins or val <= self.mins[-1]:
self.mins.append(val)
def pop(self) -> None:
if self.stack.pop() == self.mins[-1]:
self.mins.pop()
def top(self) -> int:
return self.stack[-1]
def getMin(self) -> int:
return self.mins[-1]
經典 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);count 到 k 就彈出 — LC 1047 就是 k = 2 的特例 |
Max Chunks To Make Sorted II (LC 768) — 區塊最大值堆疊
// java
// LC 768 - Max Chunks To Make Sorted II
// IDEA: monotonic increasing stack of chunk MAXIMA. A value smaller than the top must
// merge every chunk it is smaller than; the merged chunk keeps the largest max
// time = O(N), space = O(N)
public int maxChunksToSorted(int[] arr) {
Deque<Integer> stack = new ArrayDeque<>(); // chunk maxima, increasing bottom -> top
for (int num : arr) {
if (!stack.isEmpty() && num < stack.peek()) {
int maxOfMerged = stack.pop();
while (!stack.isEmpty() && num < stack.peek()) stack.pop();
stack.push(maxOfMerged); // merged chunk keeps the old max
} else {
stack.push(num); // starts a new chunk
}
}
return stack.size();
}
# python
# LC 768 - Max Chunks To Make Sorted II
# IDEA: increasing stack of chunk maxima; merging keeps the largest max
# time = O(N), space = O(N)
def maxChunksToSorted(arr):
stack = [] # chunk maxima, increasing
for num in arr:
if stack and num < stack[-1]:
merged_max = stack.pop()
while stack and num < stack[-1]:
stack.pop()
stack.append(merged_max)
else:
stack.append(num)
return len(stack)
# LC 769 - Max Chunks To Make Sorted (values are a permutation of 0..n-1)
# time = O(N), space = O(1)
def maxChunksToSortedI(arr):
chunks, running_max = 0, -1
for i, num in enumerate(arr):
running_max = max(running_max, num)
if running_max == i: # prefix holds exactly the values 0..i
chunks += 1
return chunks
2-18) Steps to Make Array Non-decreasing (LC 2289) — 把 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),所以換個角度,對每個元素問:
dp[i] = the round in which nums[i] is deleted (0 = never deleted)
answer = max(dp)
nums[i] 最終會被它左邊最近、而且嚴格大於它的元素刪掉 — 但那個「兇手」要等到兩者之間的元素全部消失,才碰得到 nums[i]。而這段等待時間,正好就是堆疊交接下來的東西:
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 是什麼意思
把堆疊想成「值/死期」的配對,會比想成索引好記得多:
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]
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。
現在把這些死期當成一張時程表讀回去,和題目敘述印出來的那幾輪對照:
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:
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
# 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
# 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
// 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 | 直接用堆疊模擬 +、D、C |
Easy |