子数组范围和 图解题解
这道题到底在问什么
- 输入
- nums = [1,2,3]
- 输出
- 4
- 输入
- nums = [4,-2,-3,4,1]
- 输出
- 59
- 输入
- nums = [1]
- 输出
- 0
最优解:一步一步想明白
- 3记住这条主线:答案 = (当最大值的贡献和) 减 (当最小值的贡献和),每份都用单调栈一遍扫出来。下面先跑第一遍:当最大值。
- 4第一遍:求每个元素当最大值的贡献。柱高 = 元素值,柱下数字 = 下标。维护一个单调栈:栈里下标对应的高度从栈底到栈顶递减。新来一个值,只要栈顶比它矮(严格小于),就说明栈顶元素当最大值的右边界到了,结算它的贡献并弹出。
- 5轮到下标 0(高 3,紫色)。先看栈顶:有没有比它矮、正等着「右边第一个更大值」的元素。
- 6栈顶不再比当前值矮(或栈已空),停止弹栈。把下标 0 压栈,让它去等自己右边第一个更大的值。当前最大值贡献和 = 0。
- 7轮到下标 1(高 1,紫色)。先看栈顶:有没有比它矮、正等着「右边第一个更大值」的元素。
- 8栈顶不再比当前值矮(或栈已空),停止弹栈。把下标 1 压栈,让它去等自己右边第一个更大的值。当前最大值贡献和 = 0。
- 9轮到下标 2(高 2,紫色)。先看栈顶:有没有比它矮、正等着「右边第一个更大值」的元素。
- 10栈顶下标 1(高 1,标红)被当前值压倒:它向左到第一个不小于它的元素的距离 left=1,向右到第一个严格大于它的元素的距离 right=1(一侧含相等、一侧不含,保证相等值不重复算)。它当最大值的子数组有 1×1=1 个,贡献 1×1×1=1。累加后最大值贡献和 = 1。弹出下标 1。
- 11栈顶不再比当前值矮(或栈已空),停止弹栈。把下标 2 压栈,让它去等自己右边第一个更大的值。当前最大值贡献和 = 1。
- 12轮到下标 3(高 4,紫色)。先看栈顶:有没有比它矮、正等着「右边第一个更大值」的元素。
- 13栈顶下标 2(高 2,标红)被当前值压倒:它向左到第一个不小于它的元素的距离 left=2,向右到第一个严格大于它的元素的距离 right=1(一侧含相等、一侧不含,保证相等值不重复算)。它当最大值的子数组有 2×1=2 个,贡献 2×2×1=4。累加后最大值贡献和 = 5。弹出下标 2。
- 14栈顶下标 0(高 3,标红)被当前值压倒:它向左到第一个不小于它的元素的距离 left=1,向右到第一个严格大于它的元素的距离 right=3(一侧含相等、一侧不含,保证相等值不重复算)。它当最大值的子数组有 1×3=3 个,贡献 3×1×3=9。累加后最大值贡献和 = 14。弹出下标 0。
- 15栈顶不再比当前值矮(或栈已空),停止弹栈。把下标 3 压栈,让它去等自己右边第一个更大的值。当前最大值贡献和 = 14。
- 16数组扫完,补一个高度无穷大的虚拟哨兵收尾:它比栈里任何元素都大,逼着把栈里还没结算的元素全部弹出结算(它们的右边界就是数组末尾)。
- 17栈顶下标 3(高 4,标红)被当前值压倒:它向左到第一个不小于它的元素的距离 left=4,向右到第一个严格大于它的元素的距离 right=1(一侧含相等、一侧不含,保证相等值不重复算)。它当最大值的子数组有 4×1=4 个,贡献 4×4×1=16。累加后最大值贡献和 = 30。弹出下标 3。
- 18哨兵把栈清空,第一遍结束:所有元素当最大值的贡献已全部累加,最大值贡献和 = 30。
- 19第一遍得到最大值贡献和 = 30。第二遍把判断方向反过来:求每个元素当最小值的贡献。这次栈里高度从栈底到栈顶递增,新来一个值,只要栈顶比它高(严格大于)就结算并弹出。贡献公式同样是 nums[j]×left×right,只是归属方向反过来:left 是向左到第一个不大于它的元素的距离、right 是向右到第一个严格小于它的元素的距离(同样一侧含相等、一侧不含,保证相等值不重复算)。
- 20轮到下标 0(高 3,紫色)。看栈顶:有没有比它高、正等着「右边第一个更小值」的元素。
- 21栈顶不再比当前值高(或栈已空),停止弹栈,把下标 0 压栈。当前最小值贡献和 = 0。
- 22轮到下标 1(高 1,紫色)。看栈顶:有没有比它高、正等着「右边第一个更小值」的元素。
- 23栈顶下标 0(高 3,标红)被当前值压低:它向左到第一个不大于它的元素的距离 left=1、向右到第一个严格小于它的元素的距离 right=1(一侧含相等、一侧不含,保证相等值不重复算)。它当最小值的子数组有 1×1=1 个,贡献 3×1×1=3。累加后最小值贡献和 = 3。弹出下标 0。
- 24栈顶不再比当前值高(或栈已空),停止弹栈,把下标 1 压栈。当前最小值贡献和 = 3。
- 25轮到下标 2(高 2,紫色)。看栈顶:有没有比它高、正等着「右边第一个更小值」的元素。
- 26栈顶不再比当前值高(或栈已空),停止弹栈,把下标 2 压栈。当前最小值贡献和 = 3。
- 27轮到下标 3(高 4,紫色)。看栈顶:有没有比它高、正等着「右边第一个更小值」的元素。
- 28栈顶不再比当前值高(或栈已空),停止弹栈,把下标 3 压栈。当前最小值贡献和 = 3。
- 29再补一个高度负无穷的虚拟哨兵收尾:它比栈里任何元素都小,逼着把栈里还没结算的元素全部弹出结算。
- 30栈顶下标 3(高 4,标红)被当前值压低:它向左到第一个不大于它的元素的距离 left=1、向右到第一个严格小于它的元素的距离 right=1(一侧含相等、一侧不含,保证相等值不重复算)。它当最小值的子数组有 1×1=1 个,贡献 4×1×1=4。累加后最小值贡献和 = 7。弹出下标 3。
- 31栈顶下标 2(高 2,标红)被当前值压低:它向左到第一个不大于它的元素的距离 left=1、向右到第一个严格小于它的元素的距离 right=2(一侧含相等、一侧不含,保证相等值不重复算)。它当最小值的子数组有 1×2=2 个,贡献 2×1×2=4。累加后最小值贡献和 = 11。弹出下标 2。
- 32栈顶下标 1(高 1,标红)被当前值压低:它向左到第一个不大于它的元素的距离 left=2、向右到第一个严格小于它的元素的距离 right=3(一侧含相等、一侧不含,保证相等值不重复算)。它当最小值的子数组有 2×3=6 个,贡献 1×2×3=6。累加后最小值贡献和 = 17。弹出下标 1。
- 33第二遍结束:所有元素当最小值的贡献已累加,最小值贡献和 = 17。
- 34两遍都跑完:最大值贡献和 = 30,最小值贡献和 = 17。答案 = 30 减 17 = 13,与开头给的 13 一致。整个过程只扫了两遍数组,每个下标进出栈各两次,所以是 O(n)。
⚠️ 容易写错的地方
✗ 错:直接枚举所有子数组逐个求范围
✓ 对:贡献法 + 单调栈
子数组有 O(n²) 个,逐个求最值是 O(n³) 或 O(n²);贡献法换成枚举元素,降到 O(n)
✗ 错:两遍用同一个弹栈条件
✓ 对:最大值遍用严格小于弹、最小值遍用严格大于弹
求最大值贡献时遇更大值才结算,求最小值贡献时遇更小值才结算,方向必须相反
✗ 错:C++ / Java 用 int 累加
✓ 对:用 long long / long
元素可达 10^9,子数组数量很多,累加会溢出 int
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def subArrayRanges(self, nums: List[int]) -> int:
n = len(nums)
def sum_max() -> int:
st = []
total = 0
for i in range(n + 1):
cur = float('inf') if i == n else nums[i]
while st and nums[st[-1]] < cur:
j = st.pop()
total += nums[j] * (j - (st[-1] if st else -1)) * (i - j)
st.append(i)
return total
def sum_min() -> int:
st = []
total = 0
for i in range(n + 1):
cur = float('-inf') if i == n else nums[i]
while st and nums[st[-1]] > cur:
j = st.pop()
total += nums[j] * (j - (st[-1] if st else -1)) * (i - j)
st.append(i)
return total
return sum_max() - sum_min()C++
#include <vector>
using namespace std;
class Solution {
public:
long long subArrayRanges(vector<int>& nums) {
return sumMax(nums) - sumMin(nums);
}
long long sumMax(vector<int>& a) {
vector<int> st; long long ans = 0; int n = a.size();
for (int i = 0; i <= n; ++i) {
long long cur = i == n ? 4000000000000000000LL : a[i];
while (!st.empty() && a[st.back()] < cur) {
int j = st.back(); st.pop_back();
ans += 1LL * a[j] * (j - (st.empty() ? -1 : st.back())) * (i - j);
}
st.push_back(i);
}
return ans;
}
long long sumMin(vector<int>& a) {
vector<int> st; long long ans = 0; int n = a.size();
for (int i = 0; i <= n; ++i) {
long long cur = i == n ? -4000000000000000000LL : a[i];
while (!st.empty() && a[st.back()] > cur) {
int j = st.back(); st.pop_back();
ans += 1LL * a[j] * (j - (st.empty() ? -1 : st.back())) * (i - j);
}
st.push_back(i);
}
return ans;
}
};Java
import java.util.*;
class Solution {
public long subArrayRanges(int[] nums) {
return sumMax(nums) - sumMin(nums);
}
private long sumMax(int[] a) {
Deque<Integer> st = new ArrayDeque<>();
long ans = 0;
for (int i = 0; i <= a.length; i++) {
long cur = i == a.length ? Long.MAX_VALUE : a[i];
while (!st.isEmpty() && a[st.peek()] < cur) {
int j = st.pop();
ans += (long) a[j] * (j - (st.isEmpty() ? -1 : st.peek())) * (i - j);
}
st.push(i);
}
return ans;
}
private long sumMin(int[] a) {
Deque<Integer> st = new ArrayDeque<>();
long ans = 0;
for (int i = 0; i <= a.length; i++) {
long cur = i == a.length ? Long.MIN_VALUE : a[i];
while (!st.isEmpty() && a[st.peek()] > cur) {
int j = st.pop();
ans += (long) a[j] * (j - (st.isEmpty() ? -1 : st.peek())) * (i - j);
}
st.push(i);
}
return ans;
}
}复杂度
时间
O(n)
两遍扫描,每遍里每个下标最多进栈一次、出栈一次,均摊 O(n);两遍合计仍是 O(n)
空间
O(n)
单调栈最坏(严格单调时)装下全部 n 个下标
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子数组范围和 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
怎么避免相等元素被重复或漏算?+
关键在两遍的弹栈条件用「严格」不等。比如最大值遍弹栈用严格小于,意味着相等的元素一边算入左边界、一边算入右边界,正好不重不漏地把每个子数组只归给唯一一个「代表最大值」的下标。一边严格、一边非严格的写法也能做到这点,核心是保证相等情况下归属唯一。
这题和「子数组最小值之和 lc907」是什么关系?+
lc907 就是本题的「最小值之和」那一半,单调栈贡献法完全一样。本题等于做两遍:一遍求最大值之和(把比较方向反过来),一遍求最小值之和,最后相减。掌握了贡献法模板,这一类题都能套。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 子数组范围和 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。