1. 程式人生 > 其它 >區間最值

區間最值

區間最值的求法通常被稱為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;
}