滑动窗口(最大最小值)的经典例题

滑动窗口简单概念

滑动窗口是我们假想出的一种数据结构,我们在这篇文章实现的窗口,能较快速的求区间最大最小值

在一些区间不回退的题目中运行效率也十分优秀

设窗口的左边界为l,右边界为r,(规定l<=r恒成立)

我们可以通过滑动右边界,从窗口的右边进入数字

也可以通过滑动左边界,从窗口左边出数字

在这里插入图片描述

滑动窗口求区间最大值(leetcode239)

原题链接

题目描述:

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。

思路求解

我们可以通过双端队列来实现我们求最大值窗口的结构,双端队列中存储数组中的下标

双端队列中必须始终维持根据下标对应的数字由大到小的顺序

不断的移动窗口,若进入的数会打破由大到小的平衡(即新数字大于队列末),队尾就不断出数字,直到满足双端队列的要求为止,再把当前数字插入

动图演示:

在这里插入图片描述

它可以表示在固定大小窗口中,只要窗口中包含这个数字,那么这个窗口的最大值永远队头

直到窗口中没有了这个最大值,那么把它从左边弹出

代码实现及注释:

class Solution {
public:
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n=nums.size();
        vector<int>ans;
        
        deque<int>qmax;//维持最大值的队列
        for(int r=0;r<n;r++)
        {
            while(!qmax.empty()&&nums[qmax.back()]<=nums[r])
            {
                qmax.pop_back();//这是维持队列由大到小的结构
                //将队尾所有比新数字小的数字全部弹出
            }
            qmax.push_back(r);

            if(qmax.front()==r-k)
            {
                qmax.pop_front();//这是最大值对应下标已经不在窗口中的情况
                //因为r-l=k
            }

            if(r>=k-1)
            {
                ans.push_back(nums[qmax.front()]);
                //窗口已经成型,就直接加入数字
            }
        }
        return ans;
    }
};

求满足要求的子数组个数

题目描述:

给定一个数组arr和一个整数sum

若一个子数组(必须连接)满足子序列最大值-最小值<=sum,则称这个子数组符合要求

返回所有符合要求的子序列个数

思路求解:

我们可以先甩出两个结论:

  • 如果某个子数组满足要求,那么这个子数组的子数组都符合要求
  • 如果某个子数组不满足要求,那么以它为子数组的数组都不符合要求

为什么?

我们知道,我们最终要求的是max-min

如果满足要求了,那么max-min<sum是应该成立的

显然易见,它的子数组最大值只会比这个数组的最大值小,最小值只会比这个数组的最小值大,

所以这个情况下,max-min是只会减少的

不满足要求的同理可证

所以我们可以这样:

初始先把窗口放在最左边,维持窗口最大和最小值,不断更新窗口右边界,直到不满足要求

根据结论1,从l开始的所有子数组都满足要求**(l到l+1;l到l+2;l到l+3…等子数组)**

根据结论1,我们在更新l的时候,可以不用更新r

代码和注释:

	int AllLessNumSubArray(vector<int>& nums, int sum)
	{
		int n = nums.size();
		deque<int>qmax;
		deque<int>qmin;
		int ans = 0;
		int r = 0;
		for (int l = 0; l < n; l++)
		{
			while (r < n)
			{
				while (!qmax.empty() && nums[qmax.back()] <= nums[r])
				{
					qmax.pop_back();
				}

				qmax.push_back(r);
				while (!qmin.empty() && nums[qmin.back()] >= nums[r])
				{
					qmin.pop_back();
				}
				qmin.push_back(r);
				//以上是维持最大最小队列
				//以下是滑动窗口

				if (nums[qmax.front()] - nums[qmin.front()] <= sum)
				{
					r++;
				}
				else
				{
					break;
				}

			}

			ans += r - l;
			//更新过期元素
			if (qmax.front() == l)
			{
				qmax.pop_front();
			}

			if (qmin.front() == l)
			{
				qmin.pop_front();
			}
		}
		return ans;
	}

以上都是题目中明确要求求窗口最大最小值的,下面这道题就不太明显了

加油站最佳出发点

题目描述

给你两个数组,gas和cost

其实gas[i]表示第i号加油站的油量,cost[i]表示从i号到i+1号加油站消耗的油量

车初始油量为0

返回一个bool类型,表示第i号加油站出发,车能不能完整绕过一圈,只能逆时针绕圈

例如:

gas=[1,1,3,1]
cost=[2,2,1,1]

图如下:

在这里插入图片描述
如果你从0号加油站出发,cost[0]是2,但gas[0]=1,车不能到1号加油站,所以ans[0]返回false

总体返回:[0,0,1,0]

思路求解:

我们在解题前,可以先进行一个加工

先将gas和cost每个位置对应数字相减,得到处理后的数组nums

for (int i = 0; i < n; i++)
{
	nums[i] = gas[i] - cost[i];
}

这样就能表示出从某一站出发,能不能到达它的下一站,如果结果为负数,则不能到达

我们可以求出前缀和,如果中途出现了负数,表示这一圈不能够完成

例如:从0开始的前缀和,表示从0出发,中途途径每个加油站的剩余油量
从1开始的前缀和,表示从1出发,以此类推

因为我们要绕一圈,所以我们应该处理出两倍的原数组大小(不从0开始出发?)

		int m=n<<1;
		for (int i = 0; i < n; i++)
		{
			nums[i] = gas[i] - cost[i];
			nums[i + n] = gas[i] - cost[i];
		}

		for (int i = 1; i < m; i++)
		{
			nums[i] += nums[i - 1];
		}

我们本题的窗口要求大小与原数组相同,我们可以这样处理我们的数组:

维持最小值窗口

显而易见,如果最小值不是负数,那么窗口中就没有负数了

在更新数组窗口时,我们记得把窗口中的值减去窗口左边界前面一个值,这样就才是正确的从l开始的前缀和

直接判断当前最小值是否小于0即可

代码即注释:

	vector<bool> GasStation(vector<int>& gas, vector<int>& cost)
	{
		int n = gas.size();
		int m = n << 1;
		vector<bool>ans(n, false);
		int* nums = new int[m];

		for (int i = 0; i < n; i++)
		{
			nums[i] = gas[i] - cost[i];
			nums[i + n] = gas[i] - cost[i];
		}

		for (int i = 1; i < m; i++)
		{
			nums[i] += nums[i - 1];
		}

		deque<int>w;
		
		//初始化窗口
		for (int i = 0; i < n; i++)
		{
			while (!w.empty() && nums[w.back()] >= nums[i])
			{
				w.pop_back();
			}
			w.push_back(i);
		}

		for (int offset = 0, i = 0, j = n; j < m; offset = nums[i++], j++)
		{
			//offset表示窗口前面的数字,w始终维持窗口最小值
			//i左边界,j右边界

			if (nums[w.front()] - offset >= 0)
			{
				ans[i] = true;
			}

			if (w.front() == i)
			{
				w.pop_front();
			}

			while (!w.empty() && nums[w.back()] >= nums[j])
			{
				w.pop_back();
			}
			w.push_back(j);

		}
		delete[]nums;
		return ans;
	}
  • 43
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 28
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 28
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

东条希尔薇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值