LeetCode 907中等单调栈
子数组的最小值之和 图解题解
这道题到底在问什么
给定数组 arr,对每个连续子数组取其最小值,把这些最小值全部相加,结果对 1e9+7 取模。
- 输入
- arr=[3,1,2,4]
- 输出
- 17 (10 个子数组的最小值累加)
最优解:一步一步想明白
- 3记住口诀:维护值递增的下标栈,弹栈即结算贡献 arr[j] × left × right。下面每帧都在套它。
- 4轮到 arr[0]=4(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 4 的下标,此刻弹出结算。当前 栈空。
- 5把 arr[0]=4 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 6轮到 arr[1]=2(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 2 的下标,此刻弹出结算。当前 栈顶 arr[0]=4。
- 7栈顶 arr[0]=4 的值 ≥ 2,弹出结算。它在绿色这段里当最小值:左边可选 1 个起点、右边可选 1 个终点,共 1×1=1 个子数组,贡献 4×1×1=4。累加和到 4。
- 8把 arr[1]=2 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 9轮到 arr[2]=6(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 6 的下标,此刻弹出结算。当前 栈顶 arr[1]=2。
- 10把 arr[2]=6 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 11轮到 arr[3]=1(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 1 的下标,此刻弹出结算。当前 栈顶 arr[2]=6。
- 12栈顶 arr[2]=6 的值 ≥ 1,弹出结算。它在绿色这段里当最小值:左边可选 1 个起点、右边可选 1 个终点,共 1×1=1 个子数组,贡献 6×1×1=6。累加和到 10。
- 13栈顶 arr[1]=2 的值 ≥ 1,弹出结算。它在绿色这段里当最小值:左边可选 2 个起点、右边可选 2 个终点,共 2×2=4 个子数组,贡献 2×2×2=8。累加和到 18。
- 14把 arr[3]=1 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 15轮到 arr[4]=5(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 5 的下标,此刻弹出结算。当前 栈顶 arr[3]=1。
- 16把 arr[4]=5 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 17轮到 arr[5]=3(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 3 的下标,此刻弹出结算。当前 栈顶 arr[4]=5。
- 18栈顶 arr[4]=5 的值 ≥ 3,弹出结算。它在绿色这段里当最小值:左边可选 1 个起点、右边可选 1 个终点,共 1×1=1 个子数组,贡献 5×1×1=5。累加和到 23。
- 19把 arr[5]=3 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 20轮到 arr[6]=7(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 7 的下标,此刻弹出结算。当前 栈顶 arr[5]=3。
- 21把 arr[6]=7 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 22轮到 arr[7]=2(紫色)。它可能成为栈内元素的「右边界」:凡栈里值 ≥ 2 的下标,此刻弹出结算。当前 栈顶 arr[6]=7。
- 23栈顶 arr[6]=7 的值 ≥ 2,弹出结算。它在绿色这段里当最小值:左边可选 1 个起点、右边可选 1 个终点,共 1×1=1 个子数组,贡献 7×1×1=7。累加和到 30。
- 24栈顶 arr[5]=3 的值 ≥ 2,弹出结算。它在绿色这段里当最小值:左边可选 2 个起点、右边可选 2 个终点,共 2×2=4 个子数组,贡献 3×2×2=12。累加和到 42。
- 25把 arr[7]=2 自己压进栈(蓝色)。现在栈里下标对应的值严格递增,等着后面更小的元素来给它定右边界。
- 26数组扫完了,栈里还剩 2 个下标没结算。补一个虚拟的「值 0 哨兵」当统一右边界,0 比谁都小,能把它们全部逼出来。
- 27栈顶 arr[7]=2 的值 ≥ 哨兵 0,弹出结算。它在绿色这段里当最小值:左边可选 4 个起点、右边可选 1 个终点,共 4×1=4 个子数组,贡献 2×4×1=8。累加和到 50。
- 28栈顶 arr[3]=1 的值 ≥ 哨兵 0,弹出结算。它在绿色这段里当最小值:左边可选 4 个起点、右边可选 5 个终点,共 4×5=20 个子数组,贡献 1×4×5=20。累加和到 70。
- 29每个元素都被弹栈结算过一次,各自当最小值时的贡献全部累加,最终答案 70。每个下标只入栈、出栈各一次,整体 O(n)。
⚠️ 容易写错的地方
✗ 错:左右边界都用严格小于
✓ 对:左边用严格小于、右边用小于等于
出现重复值时,若两边都严格,相等元素会互相把对方算进区间,导致重复计数
✗ 错:忘记取模
✓ 对:每次累加后立刻对 1e9+7 取模
arr[i] 与 n 都可达 3×10⁴,总和会超过 32 位;题目要求对 1e9+7 取模,乘法需用 long 或 long long 后再取模
✗ 错:不放哨兵,扫完就结束
✓ 对:末尾用一个值 0 的哨兵把栈清空
不结算栈里剩余元素会漏掉它们作为最小值的全部子数组
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def sumSubarrayMins(self, arr: List[int]) -> int:
MOD = 10**9 + 7
stack = []
ans = 0
for i in range(len(arr) + 1):
cur = 0 if i == len(arr) else arr[i]
while stack and arr[stack[-1]] >= cur:
j = stack.pop()
left = j - (stack[-1] if stack else -1)
right = i - j
ans = (ans + arr[j] * left * right) % MOD
stack.append(i)
return ansC++
#include <vector>
using namespace std;
class Solution {
public:
int sumSubarrayMins(vector<int>& arr) {
const long long MOD = 1000000007;
vector<int> st;
long long ans = 0;
for (int i = 0; i <= (int)arr.size(); ++i) {
int cur = i == (int)arr.size() ? 0 : arr[i];
while (!st.empty() && arr[st.back()] >= cur) {
int j = st.back(); st.pop_back();
int left = j - (st.empty() ? -1 : st.back());
int right = i - j;
ans = (ans + 1LL * arr[j] * left * right) % MOD;
}
st.push_back(i);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public int sumSubarrayMins(int[] arr) {
long MOD = 1_000_000_007L, ans = 0;
Deque<Integer> st = new ArrayDeque<>();
for (int i = 0; i <= arr.length; i++) {
int cur = i == arr.length ? 0 : arr[i];
while (!st.isEmpty() && arr[st.peek()] >= cur) {
int j = st.pop();
int left = j - (st.isEmpty() ? -1 : st.peek());
int right = i - j;
ans = (ans + (long) arr[j] * left * right) % MOD;
}
st.push(i);
}
return (int) ans;
}
}复杂度
时间
O(n)
每个下标至多入栈一次、出栈一次,总操作线性
空间
O(n)
单调栈最多同时存 n 个下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子数组的最小值之和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
能不能不用栈,直接对每个元素往左右暴力找第一个更小的边界?+
可以,但最坏(如严格递减数组)每个元素都要扫到头,退化成 O(n²)。单调栈把「找左右第一个更小」摊还成每个下标进出栈各一次,做到 O(n)。
如果题目改成求所有子数组「最大值之和」,怎么改?+
完全对称:把单调栈改成维护值严格递减的下标栈,弹栈条件改成栈顶值 ≤ cur,哨兵用一个比所有值都大的数(或正无穷)。贡献公式 arr[j]×left×right 不变。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 子数组的最小值之和 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。