RMQ問題的各種解法

lrx139發表於2024-04-05

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]\) 的最大值。

程式碼

相關文章