LeetCode 1856中等单调栈
子数组最小乘积的最大值 图解题解
这道题到底在问什么
给定正整数数组 nums。子数组的最小乘积 = min(子数组) × sum(子数组)。返回所有子数组里最小乘积的最大值(结果对 1e9+7 取模)。
- 输入
- nums=[1,2,3,2]
- 输出
- 14 (子数组 [2,3,2]:min=2、和=7、2×7=14)
最优解:一步一步想明白
- 3记住「每个元素当最小值,向两边扩到比它小为止,算 值×区间和」,下面每帧都在套它。
- 4轮到 nums[0] = 3 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 3 就纳入区间(区间里它最小)。
- 53 的最大区间是 [0, 0](绿色),和 3,最小乘积 3×3 = 9。比之前大,刷新最大为 9。
- 6轮到 nums[1] = 1 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 1 就纳入区间(区间里它最小)。
- 7向左看 nums[0] = 3,不小于 1,纳入区间,左界扩到 0。
- 8向右看 nums[2] = 5,不小于 1,纳入区间,右界扩到 2。
- 9向右看 nums[3] = 6,不小于 1,纳入区间,右界扩到 3。
- 10向右看 nums[4] = 4,不小于 1,纳入区间,右界扩到 4。
- 11向右看 nums[5] = 2,不小于 1,纳入区间,右界扩到 5。
- 121 的最大区间是 [0, 5](绿色),和 21,最小乘积 1×21 = 21。比之前大,刷新最大为 21。
- 13轮到 nums[2] = 5 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 5 就纳入区间(区间里它最小)。
- 14向右看 nums[3] = 6,不小于 5,纳入区间,右界扩到 3。
- 155 的最大区间是 [2, 3](绿色),和 11,最小乘积 5×11 = 55。比之前大,刷新最大为 55。
- 16轮到 nums[3] = 6 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 6 就纳入区间(区间里它最小)。
- 176 的最大区间是 [3, 3](绿色),和 6,最小乘积 6×6 = 36。没超过当前最大 55,保持。
- 18轮到 nums[4] = 4 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 4 就纳入区间(区间里它最小)。
- 19向左看 nums[3] = 6,不小于 4,纳入区间,左界扩到 3。
- 20向左看 nums[2] = 5,不小于 4,纳入区间,左界扩到 2。
- 214 的最大区间是 [2, 4](绿色),和 15,最小乘积 4×15 = 60。比之前大,刷新最大为 60。
- 22轮到 nums[5] = 2 当最小值。从它出发向左、向右扩展,只要邻居 ≥ 2 就纳入区间(区间里它最小)。
- 23向左看 nums[4] = 4,不小于 2,纳入区间,左界扩到 4。
- 24向左看 nums[3] = 6,不小于 2,纳入区间,左界扩到 3。
- 25向左看 nums[2] = 5,不小于 2,纳入区间,左界扩到 2。
- 262 的最大区间是 [2, 5](绿色),和 17,最小乘积 2×17 = 34。没超过当前最大 60,保持。
- 27扫完每个元素当最小值的情形,最大的一个是 nums[4]=4 管区间 [2, 4](5、6、4),4×15 = 60。答案 60(最终再对 1e9+7 取模)。
⚠️ 容易写错的地方
✗ 错:枚举所有子数组 O(n²) 甚至 O(n³)
✓ 对:枚举「谁当最小值」+ 单调栈定边界
每个元素当最小值的最大区间唯一,O(n) 即可
✗ 错:边界扩展时用 > 而不是 ≥
✓ 对:相等元素要纳入(≥),否则区间被切碎
用 > 会让相等的相邻元素各自成区间,漏掉更大的和
✗ 错:忘记最后取模 / 中途取模
✓ 对:最后再对 1e9+7 取模
中途取模会破坏「取最大值」的比较
完整代码(Python / C++ / Java)
Python
from typing import List
class Solution:
def maxSumMinProduct(self, nums: List[int]) -> int:
MOD = 10**9 + 7
prefix = [0]
for x in nums:
prefix.append(prefix[-1] + x)
stack = []
best = 0
for i in range(len(nums) + 1):
cur = nums[i] if i < len(nums) else 0
while stack and nums[stack[-1]] > cur:
mid = stack.pop()
left = stack[-1] + 1 if stack else 0
total = prefix[i] - prefix[left]
best = max(best, total * nums[mid])
stack.append(i)
return best % MODC++
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
int maxSumMinProduct(vector<int>& nums) {
const long long MOD = 1000000007LL;
int n = nums.size();
vector<long long> prefix(n + 1);
for (int i = 0; i < n; ++i) prefix[i + 1] = prefix[i] + nums[i];
vector<int> st;
long long best = 0;
for (int i = 0; i <= n; ++i) {
int cur = i < n ? nums[i] : 0;
while (!st.empty() && nums[st.back()] > cur) {
int mid = st.back(); st.pop_back();
int left = st.empty() ? 0 : st.back() + 1;
long long total = prefix[i] - prefix[left];
best = max(best, total * nums[mid]);
}
st.push_back(i);
}
return best % MOD;
}
};Java
import java.util.*;
class Solution {
public int maxSumMinProduct(int[] nums) {
long MOD = 1_000_000_007L;
int n = nums.length;
long[] prefix = new long[n + 1];
for (int i = 0; i < n; i++) prefix[i + 1] = prefix[i] + nums[i];
Deque<Integer> stack = new ArrayDeque<>();
long best = 0;
for (int i = 0; i <= n; i++) {
int cur = i < n ? nums[i] : 0;
while (!stack.isEmpty() && nums[stack.peek()] > cur) {
int mid = stack.pop();
int left = stack.isEmpty() ? 0 : stack.peek() + 1;
long total = prefix[i] - prefix[left];
best = Math.max(best, total * nums[mid]);
}
stack.push(i);
}
return (int)(best % MOD);
}
}复杂度
时间
O(n)
单调栈每个元素进出各一次 + 前缀和
空间
O(n)
前缀和数组 + 栈
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 子数组最小乘积的最大值 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
单调栈具体怎么一次求出每个元素的左右边界?+
维护一个「值递增」的下标栈。扫到 i 时,若栈顶元素值 > nums[i],说明栈顶元素的右边界就是 i(右边第一个更小),弹出它;它的左边界是弹出后新栈顶的下一个位置(左边第一个更小之后)。这样每个元素被弹出时,左右边界同时确定,配前缀和算区间和。
为什么要前缀和?+
每个元素的区间和要 O(1) 拿到,否则每次现求和又退化到 O(n²)。前缀和 prefix[r+1]−prefix[l] 一步得区间和。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄声线把 子数组最小乘积的最大值 一步步讲透(全站 0 节语音精讲,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。