区间最值

区间最值的求法通常被称为RMQ问题。
解决这类问题的方法有很多,本文主要介绍几种简单易懂且容易实现的方法。

一 朴素算法


也就是常说的枚举,枚举每个区间找出最小值/最大值,时间复杂度为 \(O(n \cdot m)\) 通常不在考虑范围之内

二 单调队列


单调队列主要用来解决一类名为 滑动窗口 的问题。
单调队列主要流程如下,当窗口中元素不满的时候直接把元素加入到窗口中,如果新加入到元素比对尾的元素更小/更大,那么对尾的元素必然不可能对答案产生贡献了,直接将该元素从窗口中删去即可。

int min_deque(){
	int h = 1 , t = 0;
	for(int i=1;i<=n;i++){
		while(h <= t && q1[h] + m <= i) h++;
		while(h <= t && a[i] < a[q1[t]]) t--;
		q1[++t] = i;
		if(i >= m) printf("%lld ",a[q1[h]]);
	}
	printf("\n");
}
int max_deque(){
	int h = 1 , t = 0;
	for(int i=1;i<=n;i++){
		while(h <= t && q2[h] + m <= i) h++;
		while(h <= t && a[i] > a[q2[t]]) t--;
		q2[++t] = i;
		if(i >= m) printf("%lld ",a[q2[h]]);
	}
}

三 ST表

ST表基于倍增和动态规划思想,首先定义数组 \(f[i,j]\) 表示从第 \(i\) 个位置开始到第 \(i+2^j-1\) 个位置这段区间的最大值/最小值。
我们可以把刚才提到的那段长度为 \(2^j\) 的区间分为两个长度为 \(2^{j-1}\) 的区间,那么状态转移方程也就很容易列出

\[f[i,j] = min(f[i][j-1],f[i+2^{j-1}][j-1]) \]

显然 \(f[i][0] = a[i]\)

接下来就是查询操作了,考虑找出一个刚好覆盖了整个区间一半的长度,即 \(log(r - l + 1)\)

所以答案就是 \(f[l][log(r - l + 1)]\)\(f[r - 2^{log(r - l + 1)} + 1][log(r - l + 1)]\) 的最小值

int main(){
	int n = read() , m = read();
	for(int i=1;i<=n;i++){
		a[i] = read();	
	}
	lg[0] = -1;
	for(int i=1;i<=n;i++){
		f[i][0] = a[i];
		lg[i] = lg[i>>1] + 1;
	}
	for(int j=1;j<=lg[n];j++){
		for(int i=1;i<=n-(1<<j)+1;i++){
			f[i][j] = max(f[i][j-1],f[i+(1<<(j-1))][j-1]);
		}
	}
	for(int i=1;i<=m;i++){
		int l = read() , r = read();
		int k = lg[r - l + 1];
		ans = max(f[l][k] , f[r - (1<<k) + 1][k]);
		printf("%lld\n",ans);
	}
	return 0;
}

四 线段树

线段树是维护此类区间问题的最常用的方法之一
处理此类无修改操作的区间问题,线段树只需要建树和查询这两个基本操作就能快速的维护区间最值。

struct node{
	int l , r , ans = INF;
}tree[N<<2];
void push_up(int p){
	tree[p].ans = min(tree[p<<1].ans , tree[p<<1|1].ans);
}
void build(int p,int l,int r){
	tree[p].l = l , tree[p].r = r;
	if(l == r){
		tree[p].ans = a[l];
		return;
	}
	int mid = l + r >> 1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p);
}
inline int query(int p,int l,int r){
	if(l <= tree[p].l && tree[p].r <= r){
		return tree[p].ans;
	}
	int mid = tree[p].l + tree[p].r >> 1;
	if(r <= mid) return query(p<<1,l,r);
	if(l >  mid) return query(p<<1|1,l,r);
	return min(query(p<<1,l,mid),query(p<<1|1,mid+1,r));
}
int main(){
	int n = read() , m = read();
	for(int i=1;i<=n;i++){
		a[i] = read();
	}
	build(1,1,n);
	for(int i=1;i<=m;i++){
		int L = read() , R = read();
		printf("%lld ",query(1,L,R));
	}
	return 0;
}

五 树状数组

树状数组的主要功能是维护区间和,在处理区间最值的时候,我们只需要用原先树状数组中用来维护区间和的数组 \(c[i]\) 维护区间最值。

本文不再赘述树状数组的基本操作,只解释一下求最值的方法

我们考虑仿照求区间和的操作将区间 \([x,y]\) 分成两个区间

  • \(y - lowbit(y) > x\) 时:显然可以将 \([x.y]\) 分成 \([x,y - lowbit(y)]\)\([y - lowbit(y) + 1 , y]\) 这两个区间,这样拆分有什么好处呢?仔细观察不难发现后者其实就是 \(c[y]\) 这样,我们就将求最值的区间直接减少了一半。

  • \(y - lowbit(y) < x\) 时:此时就不能像刚才一样拆分了,这时考虑将区间 \([x,y]\) 拆分成 \([x,y-1]\)\(a[y]\) 看上去效率不高,但是经过这样拆分之后,区间 \([x,y-1]\) 有可能满足第一种情况,所以效率其实还在我们可以接受的范围之内。

上述过程可以选择递归完成也可以循环实现

void update(int x,int k){
	a[x] = k;
	while(x <= n){
		c[x] = min(c[x] , k);
		x += lowbit(x);
	}
}
int find_min(int l,int r){
	int ans = a[r];
	while(l != r){
		r--;
		while(r - lowbit(r) >= l){
			ans = min(ans,c[r]);
			r -= lowbit(r);
		}
		ans = min(ans,a[r]);
	}
	return ans;
}
posted @ 2022-05-27 14:28  0xFF_qwq  阅读(278)  评论(0)    收藏  举报