线性数据结构(单调栈、单调队列、优先队列、ST表)

本文知识点:

  • 单调栈
  • 单调队列
  • 优先队列
  • \(\text{ST}\)\(\text{(Sparse Table)}\)

*注:本文省略了部分可用 STL 实现的线性数据结构,例如双端队列。


单调栈

单调栈即使得栈内元素具有单调性的栈。

例如有 \(6\) 个数:\(5\)\(3\)\(8\)\(5\)\(4\)\(2\)

从左往右依次入栈(这里使栈内元素递增):

  • 栈空,\(5\) 入栈。现在栈内为 \(5\)
  • 栈顶元素 \(5\) 大于 \(3\)\(3\) 入栈。现在栈内为 \(3\)\(5\)\(3\) 位于栈顶)。
  • 栈顶元素 \(3\) 小于 \(8\)\(3\) 弹出栈;栈顶元素 \(5\) 小于 \(8\)\(5\) 弹出栈,最后 \(8\) 入栈。现在栈内为 \(8\)

如此循环,最后可以保证栈内元素使递增的,要使栈内元素递减同理。

单调栈可以使用 STL 的 stack 维护,也可以使用数组+栈顶指针维护。

例题选讲

洛谷 P5788 【模板】单调栈

给定 \(n\) 个数 \(a_{1\cdots n}\),求 \(a_i\) 之后第一个大于 \(a_i\) 的数的下标,不存在答案为 \(0\)


此题中栈里存的是下标,并且需要倒序枚举每个数求答案。

for(int i=n;i>=1;i--) {
	while(!s.empty()&&a[s.top()]<=a[i]) { // 栈顶小于当前元素就弹出
		s.pop();
	}
	if(s.empty()) { // 栈空表示没有比当前元素更大的元素了
		f[i]=0;
	}else f[i]=s.top();// f[i] 存答案
	s.push(i);
}

洛谷 P1901 发射站

跟上一题差不多,两个单调栈(或是一个单调栈维护两次)分别维护两边的最近的且比它高的发射站的下标,加上能量值,最后找最大值即可。

for(int i=1;i<=n;i++) {
    // h[i] 存高度,v[i] 存能量,p[i] 存能量站增加的能量
	while(!s.empty()&&h[s.top()]<=h[i]) {
	    s.pop();
	}
	if(!s.empty()) p[s.top()]+=v[i];
	s.push(i);
 } 
 while(!s.empty()) s.pop(); //清空栈
 for(int i=n;i>=1;i--) {
      while(!s.empty()&&h[s.top()]<=h[i]) {
	    s.pop();
	}
	if(!s.empty()) p[s.top()]+=v[i];
	s.push(i);
} 

单调队列

同理,单调队列即使得栈内元素具有单调性的队列。

其操作与单调栈基本一模一样,不同的是单调队列本质是双端队列,可以弹出队头或队尾,所以使用单调队列解决的题目可以有队列长度限制。

单调队列可以使用 STL 的 deque 维护,也可以使用数组+队头队尾指针维护。

例题选讲

洛谷 P1886 滑动窗口 /【模板】单调队列


for(int i=1;i<=n;i++){
	while(!q.empty()&&a[q.front()]>a[i]){
		q.pop_front();
	}
	q.push_front(i);
	if(i>=k){ // 长度大于 k 
		while(!q.empty()&&q.back()<=i-k) q.pop_back(); //把多余的元素弹出,使之留下 k 个元素
		cout<<a[q.front()]<<" ";
	}
}

双倍经验 洛谷 P2032 扫描

三倍经验 洛谷 P1440 求m区间内的最小值


洛谷 P3512 [POI 2010] PIL-Pilots

好题,推荐做,略有思维难度而且有细节。

给定 \(n,k\) 和一个长度为 \(n\) 的序列,求最长的最大值最小值相差不超过 \(k\) 的子段。


考虑两个单调队列维护最大值、最小值下标。当最大值与最小值差大于 \(k\) 时就把两个队列储存下标较小的队列队头弹出,使得长度尽可能大,然后每次更新最大的答案。

int a[3000100];
deque<int> q1,q2; //q1 维护最大值下标,q2 维护最小值下标
int main(){
    int k,n;
	cin>>k>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	int pos=0,ans=1; // pos 需要赋值为 0,ans 需要赋值为 1
	for(int i=1;i<=n;i++) {
		while(!q1.empty()&&a[q1.front()]<=a[i]) {
			q1.pop_front();
		}
		q1.push_front(i);
		while(!q2.empty()&&a[q2.front()]>=a[i]) {
			q2.pop_front();
		}
		q2.push_front(i);
		while(a[q1.back()]-a[q2.back()]>k) {
			int x,y;
			x=q1.back(),y=q2.back();
                        //把两个队列储存下标较小的队列队头弹出
			if(x<y) {
				pos=x;
				q1.pop_back();
			}else {
				pos=y;
				q2.pop_back();
			}
		} 
		ans=max(ans,i-pos); //更新答案
	}
	cout<<ans;
	return 0;
}

单调队列也可以用于优化动态规划。

例如如下转移方程:

\[f_i=\max(f_i,\max_{j=1}^{i-1}f_j) \]

正常暴力需要 \(O(n^2)\) 时间复杂度,而使用单调队列优化只需要 \(O(n)\)(如果使用 STL 时间复杂度则为 \(O(n \log n)\),原因是 deque,所以说 \(n\) 比较大时要注意一下)。

JNSDOJ #JNSD29. 冲冲冲


优先队列

优先队列可以用 STL 的 priority_queue 实现,其入队、出队操作时间复杂度为 \(O(\log n)\)\(n\) 为队内的元素。

priority_queue<int> 是大根堆,即队首是最大值;priority_queue<int,vector<int>,greater<int> > 是小根堆,即队首是最小值。

例题选讲

洛谷 P1090 [NOIP 2004 提高组] 合并果子


贪心,每次将重量最小的两堆果子合并。

用小根堆维护即可。

while(q.size()>1) {
	int x=q.top();
	q.pop();
	int y=q.top();
	q.pop();
	ans+=(x+y);
	q.push(x+y);	
}

洛谷 P1168 中位数

给定一个长度为 \(N\) 的非负整数序列 \(A\),对于前奇数项求中位数。


使用对顶堆维护中位数。对顶堆是用一个大根堆和一个小根堆组成的数据结构。

\(mid\) 为中位数,大根堆存大于 \(mid\) 的数,小根堆存小于 \(mid\) 的数,而两个堆元素数量相同需要相同。

如果元素数量不同,则将元素数量多的堆的堆顶作为 \(mid\),原来的 \(mid\) 加入元素较少的堆。

// big 为大根堆,sml 为小根堆
for(int i=1;i<=n;i++) {
	cin>>a[i];
	if(i==1) {
		ans=a[i]; // 先确定一个中位数
		cout<<ans<<"\n";
		continue;
	} 
	if(a[i]>ans) {
		sml.push(a[i]);
	}else big.push(a[i]);
	if(i%2) {
		while(sml.size()!=big.size()) {
			if(big.size()>sml.size()) { // 分类讨论
				sml.push(ans);
				ans=big.top();
				big.pop();
			}
			else {
				big.push(ans);
				ans=sml.top();
				sml.pop();
			}
		}
		cout<<ans<<"\n";
	}
}

\(\text{ST}\)\(\text{(Sparse Table)}\)

ST 表基于倍增思想,可用于解决 RMQ 问题,即区间最大(最小)值问题。

ST 表可以做到 \(O(n \log n)\) 预处理,\(O(1)\) 查询,但不支持修改操作,如果需要修改则要使用树状数组或线段树。

区间最大问题:

\(f(i,j)\) 为区间 \([i,i+2^j-1]\) 的最大值。

显然有 \(f(i,0)=a_i\)

然后将 \(f(i,j)\) 分成两部分,因为 \([i,i+2^j-1]\) 相当于跳了 \(2^j-1\) 步,可以通过改变 \(j\) 将其分成两块,得状态转移方程:

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

对于询问 \([l,r]\),查询 \([l,l+2^s-1]\)\([r-2^s+1,r]\),其中 \(s=\lfloor \log_2(r-l+1) \rfloor\),两部分的最大值即为答案。

以上参考了 OI Wiki - ST 表


例题选讲

洛谷 P3865 【模板】ST 表 && RMQ 问题

即区间最大问题,思路如上。

#include<bits/stdc++.h>
using namespace std;
int f[100010][20],lg[100010];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		cin>>f[i][0]; // f[i][0]=a[i]
	}
	lg[1]=0;
	for(int i=2;i<=n;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,r;
		cin>>l>>r;
		int t=lg[r-l+1];
		cout<<max(f[l][t],f[r-(1<<t)+1][t])<<"\n";	
	}
	return 0;
}

洛谷 P2880 [USACO07JAN] Balanced Lineup G

给定 \(n\) 个数,求区间 \([l,r]\) 的极差。


用两个 ST 表分别维护最小值和最大值即可。

最后用区间最大值减去区间最小值。

#include<bits/stdc++.h>
using namespace std;
int f1[100010][20],f2[100010][20],lg[100010];
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++) {
		cin>>f1[i][0]; // f[i][0]=a[i]
		f2[i][0]=f1[i][0];
	}
	lg[1]=0;
	for(int i=2;i<=n;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++) {
			f1[i][j]=max(f1[i][j-1],f1[i+(1<<(j-1))][j-1]);
		}
	}
	for(int j=1;j<=lg[n];j++) {
		for(int i=1;i<=n-(1<<j)+1;i++) {
			f2[i][j]=min(f2[i][j-1],f2[i+(1<<(j-1))][j-1]);
		}
	}
	for(int i=1;i<=m;i++) {
		int l,r;
		cin>>l>>r;
		int t=lg[r-l+1];
		cout<<max(f1[l][t],f1[r-(1<<t)+1][t])-min(f2[l][t],f2[r-(1<<t)+1][t])<<"\n";	
	}
	return 0;
}
posted @ 2025-04-04 16:53  AnOIer  阅读(44)  评论(0)    收藏  举报
//雪花飘落效果