RMQ 問題:
給定一個序列,並有一些詢問,每次詢問一個區間的最大值或最小值。
下面以區間最大值為例進行講解,設序列長度為 \(N\),有 \(M\) 次查詢。
1 單調佇列
前提條件
每個查詢的區間互相不包含、離線、不能進行修改、不能在序列中增加元素。
思路
將所有查詢按左端點排序(如果不保證左端點遞增,則不用),由於互相不包含,此時右端點也遞增。剩下就是單調佇列的模板。
時間複雜度為 \(\mathcal{O}(N+Q \log Q)\)。如果保證左端點遞增,則 \(\mathcal{O}(N+Q)\)。
例題 洛谷-P1440
程式碼
#include<cstdio>
#define UP(i,a,b) for(i=a;i<=(b);++i)
#define DN(i,a,b) for(i=a;i>=(b);--i)
const int N=2e6+5;
int a[N],n,m;
template<typename T>
class queue{
private:
T a[N],*h,*t;
public:
queue():h(a),t(a){}
void push(T k){
*t=k;++t;
}
void pop_back(){
--t;
}
void pop_front(){
++h;
}
T back(){
return *(t-1);
}
T front(){
return *h;
}
bool size(){
return h!=t;
}
};
queue<int> q;
int main(){
int i;
scanf("%d%d",&n,&m);
UP(i,1,n){
scanf("%d",a+i);
}
printf("0\n");
UP(i,1,n-1){
while(q.size()&&a[q.back()]>a[i]){
q.pop_back();
}
while(q.size()&&q.front()<=i-m){
q.pop_front();
}
q.push(i);
printf("%d\n",a[q.front()]);
}
return 0;
}
2 ST 表
前提條件
不能進行修改、不能在序列中增加元素。
思路
預處理出序列的 ST 表,然後進行查詢。
時間複雜度為 \(\mathcal(O)(N \log N+Q)\)。
例題 洛谷-P3865
程式碼
#include<cstdio>
#include<algorithm>
#define UP(i,a,b) for(i=a;i<=(b);++i)
#define DN(i,a,b) for(i=a;i>=(b);--i)
using std::max;
const int N=1e6+5;
int dp[N][25],log_2[N];
int query(int l,int r){
int k=log_2[r-l+1];
return max(dp[l][k],dp[r-(1<<k)+1][k]);
}
int main(){
int n,m,i,j,l,r;
scanf("%d%d",&n,&m);
UP(i,1,n){
scanf("%d",&dp[i][0]);
if(i>1){
log_2[i]=log_2[i>>1]+1;
}
}
UP(j,1,log_2[n]){
UP(i,1,n-(1<<j)+1){
dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
}
while(m--){
scanf("%d%d",&l,&r);
printf("%d\n",query(l,r));
}
return 0;
}
3 線段樹
前提條件
不能在序列中增加元素。
思路
線段樹向上維護區間最值即可,其他與線段樹的模板相同。
時間複雜度為 \(\mathcal{O}((N+Q) \log N)\)。
例題
給定長度為 \(N\) 區間和 \(M\) 次查詢,查詢的格式為:
\(1,l,r,k\):將 \([l,r]\) 區間的所有數都加 \(k\)。
\(2,l,r\):求區間 \([l,r]\) 的最大值。
程式碼
#include<cstdio>
#include<algorithm>
#define UP(i,a,b) for(i=a;i<=(b);++i)
#define DN(i,a,b) for(i=a;i>=(b);--i)
using std::max;
const int N=1e5+5;
int a[N],n,m;
struct node{
int t,lz;
}t[N<<2];
int ls(int p){
return p<<1;
}
int rs(int p){
return p<<1|1;
}
void push_up(int p){
t[p].t=max(t[ls(p)].t,t[rs(p)].t);
}
void build(int p=1,int l=1,int r=n){
int mid=(l+r)>>1;
if(l==r){
t[p].t=a[l];t[p].lz=0;
return;
}
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
}
void tag(int k,int p,int l,int r){
t[p].t+=(r-l+1)*k;
t[p].lz+=k;
}
void push_down(int p,int l,int r){
int mid=(l+r)>>1;
if(t[p].lz){
tag(t[p].lz,ls(p),l,mid);
tag(t[p].lz,rs(p),mid+1,r);
t[p].lz=0;
}
}
void update(int x,int y,int k,int p=1,int l=1,int r=n){
int mid=(l+r)>>1;
if(x<=l&&r<=y){
tag(k,p,l,r);
return;
}
push_down(p,l,r);
if(x<=mid){
update(x,y,k,ls(p),l,mid);
}
if(mid<y){
update(x,y,k,rs(p),mid+1,r);
}
push_up(p);
}
int query(int x,int y,int p=1,int l=1,int r=n){
int mid=(l+r)>>1,res=0;
if(x<=l&&r<=y){
return t[p].t;
}
push_down(p,l,r);
if(x<=mid){
res=max(res,query(x,y,ls(p),l,mid));
}
if(mid<y){
res=max(res,query(x,y,rs(p),mid+1,r));
}
return res;
}
int main(){
int i,o,l,r,k;
scanf("%d%d",&n,&m);
UP(i,1,n){
scanf("%d",a+i);
}
build();
UP(i,1,m){
scanf("%d%d%d",&o,&l,&r);
if(o==1){
scanf("%d",&k);
update(l,r,k);
}else{
printf("%d\n",query(l,r));
}
}
return 0;
}
4 平衡樹
前提條件
無前提條件。
思路
用平衡樹實現線段樹。因為平衡樹可以插入結點,所以還可以在序列中插入元素。
時間複雜度為 \(\mathcal{O}((N+Q) \log N)\)。
這裡用 Splay 樹實現。
例題
給定長度為 \(N\) 區間和 \(M\) 次查詢,查詢的格式為:
\(1,l,r,k\):將 \([l,r]\) 區間的所有數都加 \(k\)。
\(2,x,y\):將 \([x,len]\) 中的所有元素都後移一位,並在第 \(x\) 位插入元素 \(y\)。
\(3,x\):刪除第 \(x\) 個數。
\(4,l,r\):求區間 \([l,r]\) 的最大值。
程式碼