Leetcode 795 区间子数组个数:单调栈

文章内容

https://leetcode.cn/problems/number-of-subarrays-with-bounded-maximum/description/

一、题目解析

1.1 题意

给一个整数数组 nums 和两个整数 left、right。求所有连续非空子数组中,最大值落在 [left, right] 内的子数组个数。

plain text
输入: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 下一个更大元素

要拿到每个元素的"左右第一个更大元素",用单调递减栈:

java
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] 内:

java
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 末尾哨兵

java
int[] nums = new int[inputs.length + 1];System.arraycopy(inputs, 0, nums, 0, inputs.length);nums[inputs.length] = Integer.MAX_VALUE;

在末尾补一个 Integer.MAX_VALUE。遍历到它时,所有真实元素都比它小,栈内元素全部被弹出结算。不需要在循环外另写一段"清栈"逻辑,结构统一。

四、完整代码

java
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) 空间。

觉得不错?给作者点个赞吧

分类:题解

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Leetcode 795 区间子数组个数:单调栈 · Yinchen 的博客