https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/description/
一、题目解析
1.1 题意
给一个整数数组 nums 和两个整数 left、right。求所有连续非空子数组中,最大值落在 [left, right] 内的子数组个数。
输入:nums = [2,1,4,3], left = 2, right = 3输出:3满足条件的子数组:[2], [2, 1], [3]1.2 核心思路
每个子数组有唯一的最大值(值相同时取最左边那个)。反过来,对于一个元素 nums[k],如果它的值在 [left, right] 内,有多少个子数组以它为最大值?
把所有符合条件的 k 的贡献加起来,就是答案。

nums = [2,1,4,3],left=2, right=3。值为 2 的下标 0 在范围内:左边没有更大的元素(leftBoundary = -1),右边第一个更大的元素是下标 2 处的 4(rightBoundary = 2)。在区间 (-1, 2) 也就是下标 0~1 中,2 是最大值。包含下标 0 的子数组有两个:[2] 和 [2,1]。
值为 3 的下标 3 也在范围内:左边第一个更大的元素是下标 2 处的 4,右边没有更大的元素(哨兵 MAX_VALUE)。在区间 (2, 4) 也就是下标 3 中,只有 [3] 这一个子数组把 3 作为最大值。
leftCount = k - leftBoundary(可选的子数组起点数),rightCount = rightBoundary - k(可选的子数组终点数)。满足条件的子数组起点一定在 (leftBoundary, k],终点一定在 [k, rightBoundary),组合数就是 leftCount × rightCount。
二、单调栈模板
2.1 下一个更大元素
要拿到每个元素的"左右第一个更大元素",用单调递减栈:
Deque<Integer> stack = new ArrayDeque<>();for (int i = 0; i < n; i++) { while (!stack.isEmpty() && nums[stack.peekLast()] < nums[i]) { int idx = stack.pollLast(); // nums[idx] 的右侧第一个更大元素是 nums[i] // 左侧第一个更大元素是 stack.peekLast() } stack.addLast(i);}当前元素比栈顶大时,栈顶的"右侧第一个更大元素"找到了——弹栈结算。栈内保持严格递减,所以弹出后的新栈顶就是"左侧第一个更大元素"。
2.2 弹栈拿到的边界
每次弹栈时定义:
- 被弹元素
idx:潜在的子数组最大值 - 左边界
stack.peekLast():左侧第一个更大的元素(栈空时取 -1) - 右边界
i:右侧第一个更大的元素
区间 (leftBoundary, rightBoundary) 内,nums[idx] 就是最大的——严格大于区间内其他元素(用 < 比较,相等的不会被弹出,约定由最左边的那个作为"最大值")。
三、算法详解
3.1 贡献计算
弹出一个元素 topIndex 后,判断它的值是否在 [left, right] 内:
int rightCount = i - topIndex; // 终点有 rightCount 个选择int leftBoundary = stack.isEmpty() ? -1 : stack.peekLast();int leftCount = topIndex - leftBoundary; // 起点有 leftCount 个选择ans += rightCount * leftCount; // 组合 = 包含 topIndex 的子数组数leftCount 是起点范围的长度(起点从 leftBoundary+1 到 topIndex),rightCount 是终点范围的长度(终点从 topIndex 到 i-1)。任意起点和终点的组合构成一个子数组,且 topIndex 一定是其中的最大值,乘积就是贡献。
不在 [left, right] 内的元素不产生贡献,但必须入栈——它们的任务是作为左右边界"约束"相邻元素的扩张范围。
3.2 末尾哨兵
int[] nums = new int[inputs.length + 1];System.arraycopy(inputs, 0, nums, 0, inputs.length);nums[inputs.length] = Integer.MAX_VALUE;在末尾补一个 Integer.MAX_VALUE。遍历到它时,所有真实元素都比它小,栈内元素全部被弹出结算。不需要在循环外另写一段"清栈"逻辑,结构统一。
四、完整代码
class Solution { public int numSubarrayBoundedMax(int[] inputs, int left, int right) { ArrayDeque<Integer> stack = new ArrayDeque<>(); int ans = 0; // 末尾补哨兵 Integer.MAX_VALUE,保证所有元素都能被弹出结算 int[] nums = new int[inputs.length + 1]; System.arraycopy(inputs, 0, nums, 0, inputs.length); nums[inputs.length] = Integer.MAX_VALUE; for (int i = 0; i < nums.length; i++) { // 单调递减栈:当前元素大于栈顶 → 栈顶的右边界出现 while (!stack.isEmpty() && nums[stack.peekLast()] < nums[i]) { int topIndex = stack.pollLast(); // 值在 [left, right] 内才产生贡献 if (nums[topIndex] >= left && nums[topIndex] <= right) { int rightCount = i - topIndex; // 右边可选的终点数 int leftBoundary = stack.isEmpty() ? -1 : stack.peekLast(); int leftCount = topIndex - leftBoundary; // 左边可选的起点数 ans += rightCount * leftCount; // 起终点自由组合 } } stack.addLast(i); } return ans; }}总结:
<严格小于,等值不弹出——"最左边的最大值"代表这一组子数组,后面的等值元素会在它被弹出后再结算,不会重复nums[topIndex]不在[left, right]内时跳过ans += ...,但它依然出栈、左右边界依然被使用——它在划定范围这件事上和普通元素没有区别Integer.MAX_VALUE可以用其他足够大的数替代(如10^9+1),目的就是让它比所有输入值大
复杂度:每个元素入栈出栈各一次,O(n)。栈额外 O(n) 空间。