AcWing.154 滑动窗口(单调队列)(详细!)

AcWing.154 滑动窗口

1.题目
1.1内容描述
给定一个大小为 n≤106 的数组。
有一个大小为 k 的滑动窗口,它从数组的最左边移动到最右边。
你只能在窗口中看到 k 个数字。
每次滑动窗口向右移动一个位置。
以下是一个例子:
该数组为 [1 3 -1 -3 5 3 6 7],k 为 3。
窗口位置最小值最大值
[1 3 -1] -3 5 3 6 7-13
1 [3 -1 -3] 5 3 6 7-33
1 3 [-1 -3 5] 3 6 7-35
1 3 -1 [-3 5 3] 6 7-35
1 3 -1 -3 [5 3 6] 736
1 3 -1 -3 5 [3 6 7]37
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
1.2 时空限制:1s/64MB
1.3 输入格式
输入包含两行。
第一行包含两个整数 n 和 k,分别代表数组长度和滑动窗口的长度。
第二行有 n 个整数,代表数组的具体数值。
同行数据之间用空格隔开。
1.4 输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
输入样例
8 3
1 3 -1 -3 5 3 6 7
输出样例
-1 -3 -3 -3 3 3
3 3 5 5 6 7
2.问题分析

本题是一道经典的模板题。首先我们来看一下滑动窗口的定义,对样例中的数组(不妨记为A)进行遍历,当遍历到A[2]=-1时,由于窗口大小k = 3,于是滑动窗口形成了,即为A[0]~A[2]。

我们接着向后遍历元素,为了保持窗口大小k = 3,我们在加入A[3]的同时,势必要去掉A[0],即窗口向右滑动一位。这样一直遍历,直到最后一个元素也被遍历完。

以求解最小值为例。
(1)记滑动窗口内随机两个数组下标为i和j(规定i<j),那么当滑动窗口向右移动时,若A[i]仍在滑动窗口内,A[j]也在滑动窗口内。如果恰好A[i]>A[j],那A[i]就一定不是最小值了,我们可以将其排除在外。
(2)由此,我们可以使用单调队列来解决本题。单调队列是一种数据结构,除了队列的基本特点外,其特点是队列的队头到队尾具有严格单调性,且队尾也可以出队。
(3)当滑动窗口向右移动时,我们需要把一个新的元素加入到单调队列中。为了满足单调的性质,在加入新元素时需要与队尾元素比较,因为我们要求最小值,所以当队尾元素大于新元素时,我们就将队尾元素出队(即为(1)中将A[i]排除的过程)。重复此过程,直到队尾元素小于新元素。由此我们就可以保证队列具有严格的单调性(从队头到队尾严格单调递增)。于是,当前滑动窗口的最小值即为队头的值。
(4)考虑到滑动窗口的大小是被限制为k的,所以我们还要保证单调队列中的元素都是窗口中的,所以,在队头不在滑动窗口中时,队头出队。

以题目例子(求最小值)阐述过程。

样例数组:1 3 -1 -3 5 3 6 7
滑动窗口大小k = 3
deque<int> dequeA
(1) A[0]入队,									              ->dequeA = {1}
(2) A[1]入队,                                                ->dequeA = {1,3}
(3) A[2]入队,由于A[2]<dequeA.back() = 3,于是队尾元素3出队,   
    再次比较,由于A[2]<dequeA.back() = 1,于是队尾元素1出队,   
    单调队列无元素,A[2]直接入队,                             ->dequeA = {-1}
(4) A[3]入队,由于A[3]<dequeA.back() = -1,于是队尾元素-1出队,->dequeA = {-3}
(5) A[4]入队,                                                ->dequeA = {-3,5}
(6) A[5]入队,由于A[5]<dequeA.back() = 5,于是队尾元素5出队,  
    再次比较,A[5]>dequeA.back() = -3 ,直接入队,             ->dequeA  = {-3,3}
(7) A[6]入队,                                                
     检查发现,队头已经不在窗口中(当前窗口范围为A[4]~A[6],而A[3] = -3),于是队头出队。
                                                             ->dequeA = {3,6}
(8) A[7]入队,                                                ->dequeA = {3,6,7}
3.C++代码
3.1使用STL的双端队列:
#include<iostream>
#include <deque>
using namespace std;

const int maxn = 1000001;
int Array[maxn];
deque<int> q;
int main()
{
	int N, K;
	cin >> N >> K;

	for (int i = 1; i <= N; i++)
		cin >> Array[i];

	//最小值
	for(int i = 1;i<=N;i++)
	{
		//队尾元素大于新元素时,就将队尾元素出队 
		while (!q.empty() && q.back() > Array[i])
			q.pop_back();
		
		//如果队头不在滑动窗口中,队头出队 
		//因为没有记录队头在原来数组中的位置,
		//只能用q.front() == Array[i - K]作为判断条件,
		//导致单调队列不能使用严格单调,(本来q.back() >= Array[i]) 
		//实际上判断条件应该是r<=i-K,其中r是q.front()在原来数组的位置 
		if (!q.empty() &&i - K >= 1 && q.front() == Array[i - K]) 
			q.pop_front();
		//将新元素放入队尾 
		q.push_back(Array[i]);
		//当窗口满了之后,输出队头,即最小值 
		if (i>=K)
			cout << q.front() << " ";
	}
	q.clear();
	cout << endl;

	//最大值同最小值 
	for (int i = 1; i <= N; i++)
	{
		while (!q.empty()&& q.back() < Array[i])
			q.pop_back();
			
		if (!q.empty() &&i - K >= 1 && q.front() == Array[i - K])
			q.pop_front();
			
		q.push_back(Array[i]);
		if (i>=K)
			cout << q.front() << " ";
	}
	q.clear();
	cout << endl;
	return 0;
}

运行时间:264ms
上述方法直接存储元素,不容易判断元素是否已经离开窗口,所以我们优化方法,使单调队列只存储数组的下标。于是有以下代码:

3.2下标优化
#include<iostream>
#include <deque>
using namespace std;

const int maxn = 1000001;
int Array[maxn];
deque<int> index;
int main()
{
	int N, K;
	cin >> N >> K;

	for (int i = 1; i <= N; i++)
		cin >> Array[i];

	//最小值
	for(int i = 1;i<=N;i++)
	{
		//队尾元素大于新元素时,就将队尾元素出队,这里是严格单调 
		while (!index.empty() && Array[index.back()] >= Array[i])
			index.pop_back();
		
		//如果队头不在滑动窗口中,队头出队  
		if (!index.empty() && index.front() <= i - K) 
			index.pop_front();
		//将新元素放入队尾 
		index.push_back(i);
		//当窗口满了之后,输出队头,即最小值 
		if (i>=K)
			cout << Array[index.front()] << " ";
	}
	index.clear();
	cout << endl;

	//最大值同最小值 
	for(int i = 1;i<=N;i++)
	{
		while (!index.empty() && Array[index.back()] <= Array[i])
			index.pop_back();
		
		if (!index.empty() && index.front() <= i - K) 
			index.pop_front();

		index.push_back(i);
		if (i>=K)
			cout << Array[index.front()] << " ";
	}
	index.clear();
	cout << endl;
	return 0;
}

运行时间:246ms
那么我们是否可以不使用STL容器呢,答案是肯定的,我们可以采用数组加指针的方式来模拟单调队列。

3.3数组模拟单调队列
#include<iostream>
using namespace std;

const int maxn = 10000001;

int queue[maxn],arr[maxn];
//这里queue数组存储的也是下标而非数本身 
int main(){
	int N,K;
	cin>>N>>K;
	
	for(int i = 1;i<=N;i++)
		cin>>arr[i];
		
	//设置头指针和尾指针
	//我们规定head<tail时,单调队列无元素 
	int head = 1,tail = 0;
	
	//最小值 
	for(int i = 1;i<=N;i++){
		//队尾元素大于新元素时,就将队尾元素出队,这里是严格单调 
		while(tail>=head&&arr[queue[tail]]>=arr[i])
			tail--;
			
		//如果队头不在滑动窗口中,队头出队  	
		if(tail>=head&&queue[head] <= i-K)
			head++;	
			
		//这里一定是++tail,而不是tail++ 
		queue[++tail] = i;
		if(i>=K)
			cout<<arr[queue[head]]<<" ";
	}
	cout<<endl;
	
	head = 1,tail = 0; //指针复位 
	//最大值
	for(int i = 1;i<=N;i++){
		while(tail>=head&&arr[queue[tail]]<=arr[i])
			tail--;
			
		if(tail>=head&&queue[head] <= i-K)
			head++;
			
		queue[++tail] = i;	
		if(i>=K)
			cout<<arr[queue[head]]<<" ";
	}
	cout<<endl;
	return 0;
	
} 

运行时间:204ms

对比时间我们也可以发现,存储下标比存储数字本身时间开销小,使用数组模拟比使用STL的时间开销小。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值