區間最值
區間最值的求法通常被稱為RMQ問題。
解決這類問題的方法有很多,本文主要介紹幾種簡單易懂且容易實現的方法。
- 本文分別以 P1816 忠誠 和 P3865【模板】ST表 為例解釋區間最大值/最小值的求法。
一 樸素演算法
也就是常說的列舉,列舉每個區間找出最小值/最大值,時間複雜度為 \(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][0] = a[i]\)
接下來就是查詢操作了,考慮找出一個剛好覆蓋了整個區間一半的長度,即 \(log(r - l + 1)\)
所以答案就是 \(f[l][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;
}