滑动窗口【模板题】

滑动窗口

给定一个大小为 \(n≤10^6\) 的数组。

有一个大小为 \(k\) 的滑动窗口,它从数组的最左边移动到最右边。

你只能在窗口中看到 \(k\) 个数字。

每次滑动窗口向右移动一个位置。

以下是一个例子:

该数组为 [1 3 -1 -3 5 3 6 7],\(k\)\(3\)

窗口位置 最小值 最大值
[1 3 -1] -3 5 3 6 7 -1 3
1 [3 -1 -3] 5 3 6 7 -3 3
1 3 [-1 -3 5] 3 6 7 -3 5
1 3 -1 [-3 5 3] 6 7 -3 5
1 3 -1 -3 [5 3 6] 7 3 6
1 3 -1 -3 5 [3 6 7] 3 7
你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

输入格式
输入包含两行。

第一行包含两个整数 \(n\)\(k\),分别代表数组长度和滑动窗口的长度。

第二行有 \(n\) 个整数,代表数组的具体数值。

同行数据之间用空格隔开。

输出格式
输出包含两个。

第一行输出,从左至右,每个位置滑动窗口中的最小值。

第二行输出,从左至右,每个位置滑动窗口中的最大值。

输入样例:
8 3
1 3 -1 -3 5 3 6 7

输出样例:
-1 -3 -3 -3 3 3
3 3 5 5 6 7

暴力

滑动窗口暴力解法 \(O(kn) 10^{12}\)

  • prep(i,1,n - k + 1)遍历数组
  • prep(j,i,i + k - 1)每次找到以a[i]开头的长度为k的窗口的最小值minn/最大值maxnn
点击查看代码
	prep(i,1,n - k + 1){
		int minn = a[i];
		prep(j,i,i + k - 1)minn = min(minn,a[j]);
		cout << minn << " ";
	}
	cout << nl;
	prep(i,1,n - k + 1){
		int maxnn = a[i];
		prep(j,i,i + k)maxnn = max(maxnn,a[j]);
		cout << maxnn << " ";
	}
}

优化

优化方向:

  1. 双指针尺取
  • 例如:1 3 -1 -3 5 3 6 7中共存在6个长度为3的窗口
    [1 3 -1],[3 -1 -3],[-1 -3 5],[-3 5 3],[5 3 6],[3 6 7]
    有些数没有必要进行多次重复遍历,可以对答案可能有贡献的数进行存储
  • 如何存储?
    可以用双指针尺取记录对答案可能有贡献的数的下标
  • 那么什么是对答案贡献的数呢?
  • 以取最小值为例
    窗口内最小值左边的数不会是后面窗口中的最小值,所以是一定对无答案贡献的,不需要进行存储

---2.0---

点击查看代码
	int hh = 1,tt = 0;
	prep(i,1,n){
		while((tt - hh + 1 >= k || a[i] <= a[hh]) && hh <= tt)hh ++;
		++ tt = i;
		if(i >= k)cout << a[hh] << " ";
	}

(WA)-1 -3 -3 -3 5 3
(AC)-1 -3 -3 -3 3 3

  • 原因分析:
    4--6
    3 5 3
    5--7
    5 3 6

  • 应该先判断窗口是否超载,如果超载a[hh]也是对答案无贡献的,但是hh下一个元素不一定是序列里最小的!!!

  • 如果超载就得重新对窗口中的数求最小值

  • 怎么办?

  • !!存储的只是对答案可能有贡献的,不一定就是后续的答案 -> 因此普通双指针尺取在这里是不奏效的!!

  • 优化每次需要存储的数为:
    [-1],[-3],[-3 5],[-3 5 3],[3 6],[3 6 7]

  • 见上,对有答案贡献的数的存储其实也是维护一个单调递增的队列

  • 鉴于其结构特性,优化形式采用双端队列
    [1] -> [1 3] -> {[1 3 -1] -> [3 -1 -3] -> [-1 -3 5] -> [-3 5 3] -> [5 3 6] -> [3 6 7]}
    [1] -> [1 3] ->

---3.0---

  • 遍历数组a用数组q记录所有数的下标(入队(右))
  • 然后我们通过将对答案无贡献的数出队来维护对答案有贡献的数
  • if(q中记录的下标已经超出了窗口的范围) -> 出队(左)hh++
  • while(下一个进队的数小于等于队列中左边的数) -> 出队(右)tt--
点击查看代码
//4.0
	int hh = 0,tt  = -1;
	prep(i,1,n){
		if(i - q[hh] + 1 > k && hh <= tt)hh ++;//注意是大于k而不是大于等于,因为i是本轮的i,等于k的话q[hh]还是可能对答案有贡献的
		while(a[i] <=a[q[tt]] && hh <= tt)tt --;
		q[++ tt] = i;
		if(i >= k)cout << a[q[hh]] << " ";
	}
	cout << nl;

	hh = 0, tt = -1;
	prep(i,1,n){
		if(i - q[hh] + 1 > k && hh <= tt)hh ++;
		while(a[i] >= a[q[tt]] && hh <= tt)tt --;
		q[++ tt] = i;
		if(i >= k)cout << a[q[hh]] << " ";
	}
	cout << nl;

AC代码

点击查看代码
#include<iostream>
using namespace std;

#define prep(i,a,b) for(int i = (a); i <= (b); i ++)
#define rrep(i,a,b) for(int i = (a); i >= (b); i --)

const char nl = '\n';
int n, k;

const int N = 1e6 + 10;
int a[N],q[N];

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);

	cin >> n >> k;
	prep(i,1,n)cin >> a[i];

	int hh = 0,tt  = -1;
	prep(i,1,n){
		if(i - q[hh] + 1 > k && hh <= tt)hh ++;//注意是大于k而不是大于等于,因为i是本轮的i,等于k的话q[hh]还是可能对答案有贡献的
		while(a[i] <=a[q[tt]] && hh <= tt)tt --;
		q[++ tt] = i;
		if(i >= k)cout << a[q[hh]] << " ";
	}

	cout << nl;

	hh = 0, tt = -1;
	prep(i,1,n){
		if(i - q[hh] + 1 > k && hh <= tt)hh ++;
		while(a[i] >= a[q[tt]] && hh <= tt)tt --;
		q[++ tt] = i;
		if(i >= k)cout << a[q[hh]] << " ";
	}
	cout << nl;
}


deque

定义

deque是”double ended queue”的缩写,双端队列不论在尾部或头部插入元素,都十分迅速。而在中间插入元素则会比较费时,因为必须移动中间其他的元素。双端队列是一种随机访问的数据类型,提供了在序列两端快速插入和删除操作的功能,它可以在需要的时候改变自身大小,完成了标准的C++数据结构中队列的所有功能。

Vector是单向开口的连续线性空间,deque则是一种双向开口的连续线性空间。deque对象在队列的两端放置元素和删除元素是高效的,而向量vector只是在插入序列的末尾时操作才是高效的。deque和vector的最大差异,一在于deque允许于常数时间内对头端进行元素的插入或移除操作,二在于deque没有所谓的capacity观念,因为它是动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。换句话说,像vector那样“因旧空间不足而重新配置一块更大空间,然后复制元素,再释放旧空间”这样的事情在deque中是不会发生的。也因此,deque没有必要提供所谓的空间预留(reserved)功能。

虽然deque也提供Random Access Iterator,但它的迭代器并不是普通指针,其复杂度和vector不可同日而语,这当然涉及到各个运算层面。因此,除非必要,我们应尽可能选择使用vector而非deque。对deque进行的排序操作,为了最高效率,可将deque先完整复制到一个vector身上,将vector排序后(利用STL的sort算法),再复制回deque。

deque是一种优化了的对序列两端元素进行添加和删除操作的基本序列容器。通常由一些独立的区块组成,第一区块朝某方向扩展,最后一个区块朝另一方向扩展。它允许较为快速地随机访问但它不像vector一样把所有对象保存在一个连续的内存块,而是多个连续的内存块。并且在一个映射结构中保存对这些块以及顺序的跟踪。

声明deque容器
#include<deque>  // 头文件
deque<type> deq;  // 声明一个元素类型为type的双端队列que
deque<type> deq(size);  // 声明一个类型为type、含有size个默认值初始化元素的的双端队列que
deque<type> deq(size, value);  // 声明一个元素类型为type、含有size个value元素的双端队列que
deque<type> deq(mydeque);  // deq是mydeque的一个副本
deque<type> deq(first, last);  // 使用迭代器first、last范围内的元素初始化deq
deque的常用成员函数:
deq[ ]:用来访问双向队列中单个的元素。
deq.front():返回第一个元素的引用。
deq.back():返回最后一个元素的引用。
deq.push_front(x):把元素x插入到双向队列的头部。
deq.pop_front():弹出双向队列的第一个元素。
deq.push_back(x):把元素x插入到双向队列的尾部。
deq.pop_back():弹出双向队列的最后一个元素。
deque的一些特点:
  1. 支持随机访问,即支持[ ]以及at(),但是性能没有vector好。
  2. 可以在内部进行插入和删除操作,但性能不及list。
  3. deque两端都能够快速插入和删除元素,而vector只能在尾端进行。
  4. deque的元素存取和迭代器操作会稍微慢一些,因为deque的内部结构会多一个间接过程。
  5. deque迭代器是特殊的智能指针,而不是一般指针,它需要在不同的区块之间跳转。
  6. deque可以包含更多的元素,其max_size可能更大,因为不止使用一块内存。
  7. deque不支持对容量和内存分配时机的控制。
  8. 在除了首尾两端的其他地方插入和删除元素,都将会导致指向deque元素的任何pointers、references、iterators失效。不过,deque的内存重分配优于vector,因为其内部结构显示不需要复制所有元素。
  9. deque的内存区块不再被使用时,会被释放,deque的内存大小是可缩减的。不过,是不是这么做以及怎么做由实际操作版本定义。
  10. deque不提供容量操作:capacity()和reverse(),但是vector可以。\

AC代码

点击查看代码
#include<iostream>
#include<deque>
using namespace std;

#define prep(i,a,b) for(int i = (a); i <= (b); i ++)
#define rrep(i,a,b) for(int i = (a); i >= (b); i --)

int n, k;
deque<int> q;

const char nl = '\n';
const int N = 1e6 + 10;
int a[N];

int main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);

	cin >> n >> k;
	prep(i,1,n)cin >> a[i];

	prep(i,1,n){
		if(q.size() && i - q.front() + 1 > k)q.pop_front();
		while(q.size() && a[i] <= a[q.back()])q.pop_back();
		q.push_back(i);
		if(i >= k)cout << a[q.front()] << " ";
	}

	cout << nl;

	q.clear();	//注意要重新初始化
	prep(i,1,n){
		if(q.size() && i - q.front() + 1 > k)q.pop_front();
		while(q.size() && a[i] >= a[q.back()])q.pop_back();
		q.push_back(i);
		if(i >= k)cout << a[q.front()] << " ";
	}

	cout << nl;
}
posted @ 2023-01-11 15:25  Keith-  阅读(50)  评论(0编辑  收藏  举报