每日一题 —— LC. 795 区间子数组个数

这篇博客介绍了如何使用单调栈解决寻找数组中最大元素在特定范围内的连续子数组个数的问题。通过逆向思维,从最大值出发寻找子数组,避免了暴力枚举。首先,利用单调栈求出每个元素左侧和右侧第一个大于它的位置,然后遍历数组计算符合条件的子数组数量。在处理相同元素时,还需要进行去重操作。最后,提供了C++代码实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

795. 区间子数组个数

给你一个整数数组 nums 和两个整数:leftright 。找出 nums 中连续、非空且其中最大元素在范围 [left, right] 内的子数组,并返回满足条件的子数组的个数。

生成的测试用例保证结果符合 32-bit 整数范围。

提示

  • 1 <= nums.length <= 10^5
  • 0 <= nums[i] <= 10^9
  • 0 <= left <= right <= 10^9

示例

输入:nums = [2,1,4,3], left = 2, right = 3
输出:3
解释:满足条件的三个子数组:[2], [2, 1], [3]

思路

数据范围为10^5,那么暴力枚举全部子数组一定是不可行的。

题目要求子数组的最大元素,必须在范围[left, right]内。

我们运用逆向思维,不先枚举子数组,再求最大值;而通过最大值,来求子数组。

也就是说,对于某个元素nums[i],我们计算一下,以nums[i]作为最大值的子数组有多少个,若此时nums[i]的大小恰好在[left, right]范围内,则我们对子数组的数量进行累加。


关键一:逆向思维,通过最大值,来求子数组数量


那么对于某个元素nums[i],我们如何求得以其作为最大值的子数组有多少个呢?很明显的,子数组中若要nums[i]最大,则左侧不能有比它大的,右侧也不能有比它大的。而子数组(不同于子序列)是连续的。

所以,我们只要求解一下i左侧第一个比nums[i]大的元素在什么位置,i右侧第一个比nums[i]大的元素在什么位置。求出这两个位置后,那么对于中间部分的所有元素,nums[i]就是最大值,我们就能非常容易的计算出以nums[i]为最大值的子数组的个数了。

对于求解某个数左侧第一个比其大,这样的问题,很明显的,应该用单调栈。


关键点二:单调栈


我们只需要用单调栈,通过两次遍历,求出每个数左侧第一个比其大的位置,右侧第一个比其大的位置。

然后最后再遍历一次,能够求出每个元素作为最大值的子数组的数量,对于那些满足条件的元素,进行子数组数量的累加即可。

// C++
class Solution {
public:
    int numSubarrayBoundedMax(vector<int>& nums, int left, int right) {
        // 求每个数左边, 右边, 第一个大于大于这个数的数
        int n = nums.size();
        vector<int> leftBorder(n, -1);
        vector<int> rightBorder(n, n);
        stack<int> stk;
        for (int i = 0; i < n; i++) {
            while (stk.size() && nums[stk.top()] <= nums[i]) stk.pop();
            if (stk.size()) leftBorder[i] = stk.top();
            stk.push(i);
        }
        while (stk.size()) stk.pop();
        for (int i = n - 1; i >= 0; i--) {
            while (stk.size() && nums[stk.top()] <= nums[i]) stk.pop();
            if (stk.size()) rightBorder[i] = stk.top();
            stk.push(i);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            // 不在[left,right]范围内的直接跳过
            if (nums[i] < left || nums[i] > right) continue;
            int llen = i - leftBorder[i], rlen = rightBorder[i] - i;
            ans += llen * rlen; // 乘法原理
        }
        return ans;
    }
};

到这里还没结束,因为可能有相同元素的存在,所以上面的代码会重复统计某些子数组。所以我们还需要去重。


关键点三:去重


具体去重的方式,与 907. 子数组的最小值之和 这道题一样,我们只要在一侧求解大于等于,另一侧求解大于。就能完成去重。并且,可以通过一次遍历同时求出左右两侧的值。

// C++
class Solution {
public:
    int numSubarrayBoundedMax(vector<int>& nums, int left, int right) {
        // 求每个数左边, 右边, 第一个大于大于这个数的数
        int n = nums.size();
        vector<int> leftBorder(n, -1);
        vector<int> rightBorder(n, n);
        stack<int> stk;
        for (int i = 0; i < n; i++) {
            while (stk.size() && nums[stk.top()] <= nums[i]) {
                rightBorder[stk.top()] = i;
                stk.pop();
            }
            if (stk.size()) leftBorder[i] = stk.top();
            stk.push(i);
        }
        int ans = 0;
        for (int i = 0; i < n; i++) {
            if (nums[i] < left || nums[i] > right) continue;
            int llen = i - leftBorder[i], rlen = rightBorder[i] - i;
            ans += llen * rlen;
        }
        return ans;
    }
};

本题也有另外时间复杂度更优秀的做法,参考官网题解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值