算法专题整理:滑动窗口

文章目录

  • 找长度最短的连续子序列
    • 示例1:209.长度最小的子数组
      • 思路
      • 解答
  • 找最长的连续子序列
    • 示例1:6929.数组的最大美丽值
      • 思路1:排序+滑动窗口
        • 注意点
    • 示例2:6911.不间断子数组
      • 思路
      • 使用哈希表multiset的原因:自动找到窗口最大值/最小值
      • 解答
        • 如何获得[left,right]窗口内所有大小的以right为右端点的数组
      • 总结

视频课程:同向双指针 滑动窗口【基础算法精讲 01】_哔哩哔哩_bilibili

滑动窗口也可以理解为双指针法的一种。

滑动窗口的核心在于连续!如果需要得到的结果是满足某种较为明显条件连续子数组,可以考虑使用滑动窗口来做。

通常情况下,滑动窗口是枚举右端点,然后按条件移动左端点

枚举右端点比枚举左端点方便,因为枚举右端点,移动左端点时信息是枚举过的,是已知的;移动左端点是在缩小范围,通常更好写。

如果左端点的移动条件不明或者比较复杂,就不适合用滑动窗口。

滑动窗口一般来说,是将题目转换求最短长度的子序列/最大长度的子序列问题,满足条件的子序列就是窗口,枚举所有窗口右端点的同时,更新左端点。

左端点的移动原则:找长度最短的窗口,就是只要满足条件就移动左端点;找长度最长的窗口,就是超范围了才移动左端点

找长度最短的连续子序列

示例1:209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其和 ≥ target 的长度最小连续子数组 [numsl, numsl+1, …, numsr-1, numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 10^9
  • 1 <= nums.length <= 10^5
  • 1 <= nums[i] <= 10^5

思路

本题是很经典的滑动窗口题目,选取特定条件下的连续子数组,且为找最小长度子序列问题。因此本题是只要while循环满足条件,就更新左端点,从而使得最后得到的窗口长度是最小值

本题特定条件并不复杂,是求和的条件,并不需要单独写函数。只要满足特定条件,就可以进行滑动。

解答

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int i=0;
        int sum=0;
        int subLength=0;
        int result = INT_MAX;
        //开始滑动,直接用[i,j]作为窗口
        for(int j=0;j<nums.size();j++){
            sum += nums[j];
            //只要满足条件,就不断滑动,方便取最小值
            while(sum>=target){
                subLength = j-i+1;
                result = result<subLength?result:subLength;
                sum-=nums[i];
                i++;
            }
        }
        //如果一直没找到,结果置零
		if(result==INT_MAX){
            result=0;
        }
        return result;
    }
};

找最长的连续子序列

示例1:6929.数组的最大美丽值

给你一个下标从 0 开始的整数数组 nums 和一个 非负 整数 k

在一步操作中,你可以执行下述指令:

  • 在范围 [0, nums.length - 1] 中选择一个 此前没有选过 的下标 i
  • nums[i] 替换为范围 [nums[i] - k, nums[i] + k] 内的任一整数。

数组的 美丽值 定义为数组中由相等元素组成的最长子序列的长度。

对数组 nums 执行上述操作任意次后,返回数组可能取得的 最大 美丽值。

注意: 你只能对每个下标执行 一次 此操作。

数组的 子序列 定义是:经由原数组删除一些元素(也可能不删除)得到的一个新数组,且在此过程中剩余元素的顺序不发生改变。

示例 1:

输入:nums = [4,6,1,2], k = 2
输出:3
解释:在这个示例中,我们执行下述操作:
- 选择下标 1 ,将其替换为 4(从范围 [4,8] 中选出),此时 nums = [4,4,1,2]- 选择下标 3 ,将其替换为 4(从范围 [0,4] 中选出),此时 nums = [4,4,1,4] 。
执行上述操作后,数组的美丽值是 3(子序列由下标 013 对应的元素组成)。
可以证明 3 是我们可以得到的由相等元素组成的最长子序列长度。

示例 2:

输入:nums = [1,1,1,1], k = 10
输出:4
解释:在这个示例中,我们无需执行任何操作。
数组 nums 的美丽值是 4(整个数组)。

提示:

  • 1 <= nums.length <= 105
  • 0 <= nums[i], k <= 105

思路1:排序+滑动窗口

因为美丽值的定义是数组中由相等元素组成的最长子序列的长度,因此我们想要获得全部都是相等元素的序列,并求最大长度

虽然本题要求 子序列 定义是剩余元素的顺序不发生改变,但是求的是相等元素组成的最长子序列长度,因此元素的顺序并不影响结果

因此我们可以先进行排序,让相等的元素排在一起,此时只需要判断窗口左端点(最小值)和右端点(最大值)是不是相等,也就是只有nums[r]-k<=nums[l]+k的时候,才作为有效窗口

此时,本题就转变成了,求满足nums[r]-k<=nums[l]+k条件的最长子序列,也就是找最长的连续子数组,其最大值-最小值不超过2k

也属于找最长子序列问题。最长子序列需要枚举右端点,只有不满足条件的时候,才更新左端点。

class Solution {
public:
    int maximumBeauty(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end());
        int res=0;
        for (int l = 0, r = 0; r < nums.size(); ++r) {
            while (nums[r]-k > nums[l] + k) {
            	l++;
            }
            res = max(res, r - l + 1);
        }
        return res;
    }
};

注意点

由于选的是子序列,且子序列的元素都相等,所以元素顺序对答案没有影响,可以先对数组排序

且仔细看用例1可以看出,并不要求最后的子数组在原数组也是连续的只是找替换后相等的数字组成的总长度,并不是找原数组中就连续的子数组

参考题解:
灵茶山艾府
力扣周赛总结

示例2:6911.不间断子数组

给你一个下标从 0 开始的整数数组 numsnums 的一个子数组如果满足以下条件,那么它是 不间断 的:

  • ii + 1 ,…,j 表示子数组中的下标。对于所有满足 i <= i1, i2 <= j 的下标对,都有 0 <= |nums[i1] - nums[i2]| <= 2

请你返回 不间断 子数组的总数目。

子数组是一个数组中一段连续 非空 的元素序列。

示例 1:

输入:nums = [5,4,2,4]
输出:8
解释:
大小为 1 的不间断子数组:[5], [4], [2], [4] 。
大小为 2 的不间断子数组:[5,4], [4,2], [2,4] 。
大小为 3 的不间断子数组:[4,2,4] 。
没有大小为 4 的不间断子数组。
不间断子数组的总数目为 4 + 3 + 1 = 8 。
除了这些以外,没有别的不间断子数组。

示例 2:

输入:nums = [1,2,3]
输出:6
解释:
大小为 1 的不间断子数组:[1], [2], [3] 。
大小为 2 的不间断子数组:[1,2], [2,3] 。
大小为 3 的不间断子数组:[1,2,3] 。
不间断子数组的总数目为 3 + 2 + 1 = 6

提示:

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

思路

这道题目也是一道典型的滑动窗口题,要求找出所有满足特定条件连续子数组的个数。特定条件是,对于子数组中的任意两个元素,他们的绝对值差不能超过 2。

本题定义窗口满足绝对值差<=2的条件,且题目要求子数组连续,也就是不能改变原有元素顺序,因此我们不能直接对原数组进行排序求出绝对值<=2的连续子序列。但是我们依旧可以用滑动窗口right-left+1来包含所有的,区间内的子数组的情况

因为本题求的是符合条件的子数组的总数,所以只有令窗口最大,才能令窗口右端点-左端点+1这个长度包含所有的符合条件的情况

因此本题可以转换为,求解满足 0 <= |nums[i1] - nums[i2]| <= 2 的最大长度的滑动窗口

使用哈希表multiset的原因:自动找到窗口最大值/最小值

题目中要求但是排序后数组元素的顺序发生改变,会导致子数组与原数组中的子数组不再一致。

因为原题目中,要求的是子数组任意两个元素绝对值差不超过2。因此,我们就需要知道子数组的最大元素和最小元素之间的差值

为了节省时间复杂度,防止我们每次找最大值和最小值都需要重新遍历,本题我们最好采用可重复有序哈希表multiset/multimap来实现。

因为本题nums是存在重复元素的!因此我们只能选取key可重复的哈希表允许重复的哈希表只有multiset和multimap。因为并不涉及次数统计,所以采用multiset。

解答

  • 窗口实际上就是数组内的下标[left,right],只是把这些元素都放到mset里面进行储存了
class Solution {
public:
    long long continuousSubarrays(vector<int> &nums) {
        long long result=0;
        //创建一个multiset作为窗口,此处不是数组,因为set方便比较大小
        multiset<int>mset;
        int left=0;
        //nums.size()不是nums.end()
        for(int right = 0;right<nums.size();right++){
            //加入窗口
            mset.insert(nums[right]);
            //set默认升序,最大值在rbegin
            while(*mset.rbegin()-*mset.begin()>2){
                mset.erase(mset.find(nums[left]));
                left++;
            }
            //以right为右端点,左端点在[left,right]范围内任意一点都满足条件
            //这里的result要进行累加!
            result += right-left+1;
        }
        return result;
    }
};

如何获得[left,right]窗口内所有大小的以right为右端点的数组

因为本题需要输出所有大小的数组个数,加上是遍历right,因此我们的策略是对于每一个right找到右端点为right的所有大小的子数组

例如[1,2,3],所有以right=3为右端点的子数组,就是[1,2,3][2,3],[3]

也就是说,对于每一个right,[left,right]内包括所有大小的,以right为右端点的数组个数,是right-left+1

因此result会在每一个right遍历的时候进行累加,得到**[left,right]窗口内所有大小的数组个数**。

总结

如果题目要求的是在原数组中就连续的子数组,那么不能进行排序的操作,需要借助multiset等容器来完成窗口内部的排序。

但是如果题目求的数组,并不是必须在原数组中就连续的,就可以考虑直接排序,让符合条件的元素相邻在一起。

你可能感兴趣的:(算法模板与专题整理,算法,leetcode,数据结构,c++)