引例: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;
}