单调队列——定长区间最值问题

本文同步发表于我的 洛谷博客

一、引入

1.1 定长滑窗问题:

给出一列 n n n 个正整数,和一个固定长度为 k k k 的滑动窗口, 从左到右在数列中滑动这个窗口,找到数列中每个窗口内的最大值。

即对于给定数列 A 1 ∼ A n A_1\sim A_n A1An ( 1 ≤ n ≤ 1 0 7 ) (1\le n\le 10^7) (1n107),求每一个 f i = min ⁡ j = i − k + 1 i A j f_i=\min\limits_{j=i-k+1}^{i}A_j fi=j=ik+1miniAj

二、数据维护

2.0 线段树,RMQ

这两者与 2.1 都是 Θ ( n log ⁡ n ) \Theta(n\log n) Θ(nlogn) 级别的复杂度,且代码量较大,因此略过。

2.1 多重集

2.1.1 简介

multiset 是 STL 容器中的一种,中文名为多重集。多重集是按照特定顺序存储元素的容器,其中多个元素可以具有相等的值。

其底层数据结构为红黑树,是一种高度平衡的二叉搜索树,因此插入,搜索和删除都是 Θ ( log ⁡ n ) \Theta (\log n) Θ(logn) 的复杂度,而对于根节点的访问则是 Θ ( 1 ) \Theta(1) Θ(1) 的。

2.1.2 解决方法

每一次插入新的元素,并删除旧元素。由于 multiset 自动按从小到大进行排序,因此一段定长区间的最大值即为 *s.rbegin()

2.1.3 代码

multiset<int,greater<int> > s; //从大到小排序,不去重 
inline void Multiset(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&x[i]);//input
	for(int i=1;i<=n;i++){
		s.insert(x[i]);//插入
		if(s.size()>k) s.erase(s.find(x[i-k]));//删除元素维持定长
		ans[i]=*s.begin();//记录最值
	}
	for(int i=k;i<=n;i++) printf("%d ",ans[i]);
}

2.2 单调队列

2.2.1 简介

这时候大家一定会想:有没有更有的算法复杂度(比如线性)呢?答案是肯定的。这时候就需要单调队列发挥作用了。

单调队列,即单调递减或单调递增的队列,是一种特殊的双端队列 (deque),有以下两种操作:

  1. 插入:将新元素从队尾插入,若破坏单调性则重复删除队尾元素再插入。

  2. 查询最值:访问队首元素。

为了方便起见,队列中存储元素在原数列的下标(编号),利于维持定长。

2.2.2 操作

q [ i ] q[i] q[i] 表示单调队列 i 号元素在原 x 数组的编号

a n s [ i ] ans[i] ans[i] 表示以 i 结尾的窗口里的最值

  • 发现 i − q [ l ] > = k i-q[l] >= k iq[l]>=kl++ ∣   l < r |\ l<r  l<r //左删维持定长

  • 发现 x [ i ] > = x [ q [ r − 1 ] ] x[i]>=x\left[q[r-1]\right] x[i]>=x[q[r1]]r-- ∣   l < r |\ l<r  l<r //右删维持单调性

  • 将元素插入 q[r++] = i

很明显,由于每个元素最多出队一次,进队一次,因此时间复杂度是 Θ ( n ) \Theta(n) Θ(n)

2.2.3 正确性及性质

对于相邻两个区间 ( L , R ) (L,R) (L,R) 以及 ( L + 1 , R + 1 ) (L+1,R+1) (L+1,R+1) 有以下性质:

对于序列 A 1 , A 2 ⋯ A n A_1,A_2\cdots A_n A1,A2An,区间 ( L , R ) (L,R) (L,R)

min ⁡ { A L ∼ A R } = min ⁡ { A L , min ⁡ { A L + 1 ∼ A R } } \min\{A_L\sim A_R\}=\min\left\{A_L,\min\{A_{L+1}\sim A_R\}\right\} min{ALAR}=min{AL,min{AL+1AR}}

min ⁡ { A L + 1 ∼ A R + 1 } = min ⁡ { A R + 1 , min ⁡ { A L + 1 ∼ A R } } \min\{A_{L+1}\sim A_{R+1}\}=\min\left\{A_{R+1},\min\{A_{L+1}\sim A_R\}\right\} min{AL+1AR+1}=min{AR+1,min{AL+1AR}}

两个方程有相同的部分 min ⁡ { A L + 1 ∼ A R } \min\{A_{L+1}\sim A_R\} min{AL+1AR},因此其最值落在 ( L + 1 , R ) (L+1,R) (L+1,R) 的概率极大,那么在求 ( L + 1 , R + 1 ) (L+1,R+1) (L+1,R+1) 的最值时没有必要重新扫描一遍,只有当最值为 A l A_l Al 才需重新扫描。这样,算法得到了极大的优化。

同时,对于任意的 [ i , j ] ∈ [ L , R ] [i,j]\in [L,R] [i,j][L,R],若 A i < A j A_i<A_j Ai<Aj,在区间右移时,最值永远也不会落在 A i A_i Ai 上,因此 A i A_i Ai A j A_j Aj 先失效。这个性质与单调队列性质相符。

2.2.4 代码

例题:滑动窗口

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+5;
int n,k,x[maxn],ans[maxn];
deque<int> q; //双端队列
int main(){
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++) scanf("%d",&x[i]);
	
	for(int i=1;i<=n;i++){
		while(!q.empty() && i-q.front()>=k) q.pop_front();//左删 
		while(!q.empty() && x[i]<x[q.back()]) q.pop_back();//右删
		q.push_back(i);//插入元素编号 
		ans[i]=x[q.front()];//记录最小值 
	}
	for(int i=k;i<=n;i++) printf("%d ",ans[i]);
	
	q.clear();
	memset(ans,0,sizeof(ans)); 
	putchar('\n');//初始化 + 换行 
	
	for(int i=1;i<=n;i++){
		while(!q.empty() && i-q.front()>=k) q.pop_front();//左删 
		while(!q.empty() && x[i]>x[q.back()]) q.pop_back();//右删 
		q.push_back(i);//插入元素编号 
		ans[i]=x[q.front()];//记录最大值 
	}
	for(int i=k;i<=n;i++) printf("%d ",ans[i]);
	return 0; 
} 
已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页