线性数据结构(单调栈、单调队列、优先队列、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 维护,也可以使用数组+栈顶指针维护。
例题选讲
给定 \(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);
}
跟上一题差不多,两个单调栈(或是一个单调栈维护两次)分别维护两边的最近的且比它高的发射站的下标,加上能量值,最后找最大值即可。
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 维护,也可以使用数组+队头队尾指针维护。
例题选讲
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()]<<" ";
}
}
洛谷 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;
}
单调队列也可以用于优化动态规划。
例如如下转移方程:
正常暴力需要 \(O(n^2)\) 时间复杂度,而使用单调队列优化只需要 \(O(n)\)(如果使用 STL 时间复杂度则为 \(O(n \log n)\),原因是 deque,所以说 \(n\) 比较大时要注意一下)。
优先队列
优先队列可以用 STL 的 priority_queue 实现,其入队、出队操作时间复杂度为 \(O(\log n)\),\(n\) 为队内的元素。
priority_queue<int> 是大根堆,即队首是最大值;priority_queue<int,vector<int>,greater<int> > 是小根堆,即队首是最小值。
例题选讲
贪心,每次将重量最小的两堆果子合并。
用小根堆维护即可。
while(q.size()>1) {
int x=q.top();
q.pop();
int y=q.top();
q.pop();
ans+=(x+y);
q.push(x+y);
}
给定一个长度为 \(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\) 将其分成两块,得状态转移方程:
对于询问 \([l,r]\),查询 \([l,l+2^s-1]\) 与 \([r-2^s+1,r]\),其中 \(s=\lfloor \log_2(r-l+1) \rfloor\),两部分的最大值即为答案。
以上参考了 OI Wiki - ST 表。
例题选讲
即区间最大问题,思路如上。
#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;
}

浙公网安备 33010602011771号