Sliding Window-RMQ(区间最值问题)

博客介绍了滑动窗口问题及其在区间最值查询(RMQ)中的应用。讨论了四种不同的解决方案:线段树、单调队列、ST表(稀疏表)和数列分块。其中,单调队列维护区间单调性,ST表基于倍增思想预处理查询,数列分块将数组划分为小块进行高效处理。文章提供了相关代码示例和操作分析。
摘要由CSDN通过智能技术生成

引例:Sliding Window

给你一个长度为N的数组,一个长为K的滑动的窗体从最左移至最右端,你只能见到窗口的K个数,每次窗体向右移动一位。你的任务是找出窗口在每个位置时的max value,min value。

首先来分析一下滑动窗口的特点:
 ——大小固定
 ——单向移动
 ——单元数目不变
可以看出,这是一个典型的区间最值问题,即RMQ(Range Minimum/Maximum Query)。
那么我们来看看RMQ的四种实现方法。

类型空间复杂度时间复杂度
线段树灵活,通用性强,能处理几乎所有结构固定的数列问题代码复杂,调试麻烦O(nlgn)查询、修改:O(lgn)
单调队列高效,代码简单适用范围限于区间有连续移动特性的问题O(n)查询、修改:O(1)
ST表高效、代码简单、通用性较高需要预处理,只能面向静态数列,无法在线处理O(nlgn)查询:O(1) 
预处理:O(n*log2n)
数列分块通用性强,代码简单性能较低O(n+√n)查询:O(√n) 
修改:O(√n) 
a.线段树

本文不详细展开。点击跳转到 线段树专题文章

b.单调队列

  单调队列和普通队列一样,基本操作原则是先进先出,但是区别于一般队列的是它的内容要求具有单调性。以单调上升为例,为了维护队列的单调性,在元素入队时就做特殊处理:
入队元素不断和队尾元素比较,较小则放弃原队尾,直到队列空或队尾原素小于插入的新元素,新元素才入队。
e.g.
插入元素:46,队列:2 15 47 48 79
插入后单调队列: 2 15 46
代码奉上:

const int nn=1e5+5;
int n,k,a[nn],qa[nn],qb[nn],ah,at,bh,bt;
int main(){
	scanf("%d%d",&n,&k);
	for(int x,i=1;i<+;i++{
		scanf("%d",&a[i]);
		whlie(at&&a[qa[at]]>a[i])
			at--;//在单调队列里寻找位置
		qa[++at]=i;//插入队列,清空该位置后面的元素
		while(qa[ah]<=i-k)ah++;//将不在范围的点出队列
		if(i>2)printf("%d",a[qb[bh]]);
	}
	printf("\n");
	for(int i=1;i<=n;i++){
		while(bt&&a[qb[bt]]<a[i])bt--;
		qb[++bt]=i;
		while(qb[bh]<=i-k)bh++;
		if(i>2)printf("%d",a[qb[bh]]);
	}
	return 0;
}
c.ST表

ST(Sparse Table)算法思想:
  对于区间最值的查询rmq(i,j),通过两个并不严格独立的子区间查询结果rmq(i,j’)和rmq(i’,j);一般RMQ的ST算法是基于倍增思想的设计的O(Nlog2N) – O(1) Online算法。
  算法记录从每个元素的连续的长度为2k的区间中元素的最小值,并以在常数时间内解决询问。
请添加图片描述
对于RMQ(A,i,j)我们可以通过找到两段极大的长度为2k的区间(如图)覆盖[ i, j ]由已经计算的结果在O(1)的时间内解决询问。请添加图片描述
为了实现快速查询,需要先预处理(但因此ST无法做到在线处理)任意2k长度的区间查询值:
1.用O(N)时间处理单位长度的区间;
2.对于长度为2k的区间的最小值,由两个2k-1的区间最小值得到。
请添加图片描述

设:st [i] [k]表示数列中起点为i,长度为2k的区间最值,a[i]为元素i的值。可得状态转移方程:
st [i] [0]=a[i]
st [i] [k]=max( st [i] [k-1], st [i+2k-1] [k-1] )

利用倍增思想,在O(nlog2n)的时间内,我们可以预处理所有长度为2的幂的区间的最小值。
代码奉上:

void pre_lg( ){
	int p=-1;
	for(int i=1;i<=n;i++){
//预处理指数幂函数lg值,也可以直接计算,如下void pre_lg2()
		if( i==( 1<<(p+1) ) )p++;
		lg[i]=p;
	}
}
void pre_lg2(){
	for(int i=1;i<=n;i++)
		lg[i]=ceil(log2(i));
}
void pre_ST( ){//预处理ST表
	for(int i=1;i<=n;i++)st[i][0]=a[i];
	for(int k=1; k<=lg[n]; k++) {
		for(int i=1; i<=n-(1<<k)+1; i++)
			st[i][k]=max(st[i][k-1],st[i+(1<<k-1)][k-1]);
	}
}

int query(int x,int y) { //查询区间最大值
	int l=lg[y-x+1];
	return max(st[x][l],st[y-(1<<l)+1][l]);

}

看完ST通用模板,来看看完整代码:

#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
const int nn=1e5+5;
int st[nn][30],a[nn],lg[nn],n,m;
void preLg(){
	for(int i=1;i<=n;i++)
		lg[i]=ceil(log2(i));
}
void preST(){
	for(int i=1;i<=n;i++) st[i][0]=a[i];
	for(int j=1;j<=(int)log2(n);j++)
		for(int i=1;i<=n;i++)
			st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);
}
inline int query(int x,int y){
   int len=lg[y-x+1];
	return st[x][len-1]+st[y-(1<<len-1)][len-1]];
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	preST();
	for(int x,y,i=0;i<m;i++){
		scanf("%d%d",&x,&y);
		printf("%d\n",query(x,y));
	}
	return 0;
}
d.数列分块

数列分块的思想很简单,就是将数列平均划分为很多小的块,处理的时候基于分治思想。

问题在于,分成多少块呢?

通常的做法是:平均分为√n块,每块的长度√n(最后一块长度可能少于√n,至于为什么大家好好思考一下)。
存储上,要在原始数组的基础上增加一个block数组,用来维护每个块的信息:
int a[nn];//原始数组
int block[n];//分块维护数组,长度只需要原始数组的√nn;
int pos[n * n]; //为了更好的定位块,为原数组每个单元增加一个块指向记录。
操作上,分块数组block[n] 和原数组a[nn] 配合使用:
1.修改一个区间,如果block[i]∈[x,y],则只修改block[i]的记录,否则直接在原数组上处理;
2.查询一个区间,如果block[i]∈[x,y],则从block[i]取值,否则直接在原数组上取值。
有点难理解,上个例题吧:

数列操作
给出一个长为 n 的数列,以及 n 个操作,操作涉及区间加法,单点查值。
第一行输入一个数字 n。 第二行输入 n 个数字,第 i 个数字为 ai​,以空格隔开。
接下来输入 n 行询问,每行输入四个数字 opt、l、r、c,以空格隔开。
若 opt=0,表示将位于 [l,r] 的之间的数字都加 c。
若 opt=1,表示询问 ar​ 的值(l 和 c 忽略)。

分析:
设a[nn]为原数组,block[n]为分块数组,pos[nn]为分块定位数组。
区间修改操作:左右边界所在块在原数组a上处理,其他覆盖完整块的部分在分块数组block上处理。
单点查询操作:将原数组和对应分块的值加起来返回。
时间复杂度:块+原数组=O(√n)+O(√n),所以总的时间是O(√n)的。
详细解析参见《分块九讲》
代码奉上:

const int nn=1e5+5;
int a[nn],block[1005],n,len;
void insert(int ll,int rr,int c){
	int pl=ll/len;
	int pr=rr/len;
	if(ll%len)pl++;
	if(rr%len)pr++;
	for(int i=pl+1;i<pr;i++) block[i]+=c;
            if(pl==pr)for(int i=ll;i<=rr;i++) a[i]+=c;
            else{	for(int i=ll;i<=pl*len;i++) a[i]+=c;
		for(int i=pr*len-len;i<=rr;i++) a[i]+=c
              }
}
int query(int k){
	int p=k/len;
	if(p%len)p++;
	return a[k]+block[p];
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	len=sqrt(n);
	for(int opt,l,r,c,i=0;i<n;i++){
		scanf("%d%d%d%d",&opt,&l,&r,&c);
		if(opt) printf("%d\n",query(r));
		else insert(l,r,c);		
	}
	return 0;
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值