目录
- §单调栈
- 单调栈练习题一:[洛谷-P5788 【模板】单调栈](https://www.luogu.com.cn/problem/P5788)
- 单调栈练习题二:[牛客-Bad Hair Day](https://ac.nowcoder.com/acm/problem/25084)
- 单调栈练习题三:[洛谷-P2659 美丽的序列](https://www.luogu.com.cn/problem/P2659)
- 单调栈练习题四:[牛客-区区区间间间](https://ac.nowcoder.com/acm/problem/20806)
- 单调栈练习题五:[洛谷-P3467 \POI2008\PLA-Postering](https://www.luogu.com.cn/problem/P3467)
- 单调栈练习题六:[HDU1506-Largest Rectangle in a Histogram](http://acm.hdu.edu.cn/showproblem.php?pid=1506)
- 单调栈练习题七:[牛客-删删删越小越好](https://ac.nowcoder.com/acm/contest/11471/E)
- §单调队列
- 单调队列练习题一:[洛谷-P2032 扫描](https://www.luogu.com.cn/problem/P2032)
- 单调队列练习题二:[洛谷-P1886 滑动窗口 /【模板】单调队列](https://www.luogu.com.cn/problem/P1886)
- 单调队列练习题三:[洛谷-P1714 切蛋糕](https://www.luogu.com.cn/problem/P1714)
- 单调队列优化DP一:[洛谷-P1725 琪露诺](https://www.luogu.com.cn/problem/P1725)
- 单调队列优化DP二:[牛客-Tower of Hay](https://ac.nowcoder.com/acm/problem/24881)
- 单调队列优化DP三:[牛客-\SCOI2010\股票交易](https://ac.nowcoder.com/acm/problem/20280)
§单调栈
Usage:单调栈可以找到从左/右遍历第一个比当前元素小/大的元素的位置,以及比当前元素小/大的元素的数量
1.单调递增栈,则从栈顶到栈底的元素是严格递增的——stack.top()最小。
2.单调递减栈,则从栈顶到栈底的元素是严格递减的——stack.top()最大。
注:我们存放的一般是下标,而不是元素。但是作为比较的标准是下标对应的元素。
//模板:
for(int i=1;i<=n;i++)
{
//单调递增栈,栈顶是最小的
while(stk.size() && a[stk.top()] <= a[i])//根据情况改变比较符号
stk.pop();
//记录状态答案
stk.push(i);
}
单调栈练习题一:洛谷-P5788 【模板】单调栈
题意:求每个元素a【i】右方(i+1 to n),第一个值比它大的元素下标。
ps:upper_bound的做法放一边,这里不做考虑。
代码一:单调栈模板写法,为了得到每个位置对应右侧的第一个下标,倒着遍历,维护单调递增栈(栈顶最小),每个位置的答案就对应此时栈顶存放的下标(栈空即为0)。
int n,m,a[maxn],ans[maxn];
stack<int> stk;
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=n;i>=1;i--)
{
while(stk.size() && a[stk.top()]<=a[i])
stk.pop();
if(stk.empty())
ans[i] = 0;
else
ans[i] = stk.top();
stk.push(i);
}
for(int i=1;i<=n;i++) cout<<ans[i]<<' ';
}
代码二:伪单调栈写法:单调栈思想—向右不断延伸,数组r【i】记录的是,值不大于a【i】的元素下标最大是多少。
int n,m,k,q,a[maxn],ans[maxn];
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=n;i>=1;i--)
{
int st=i;
while(st<n && a[st+1]<=a[i])
st=ans[st+1];
ans[i]=st;
}
for(int i=1;i<=n;i++)
{
if(ans[i]+1>n)
cout<<0;
else cout<<ans[i]+1;
cout<<' ';
}
}
单调栈练习题二:牛客-Bad Hair Day
题意:找出对于每个a【i】,从右方起,连续且小于它的元素最多有多少个,将所有个数相加为答案。
代码:和上题类似,但不需要输出每个点的答案的话,正序遍历,维护一个单调递增栈(栈顶最小),总贡献即为每次遍历,压栈前,栈的大小。
ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
ll ans=0;
for(int i=1;i<=n;i++)
{
while(stk.size() && a[stk.top()]<=a[i])
stk.pop();
ans += stk.size();
stk.push(i);
}
cout<<ans<<endl;
}
单调栈练习题三:洛谷-P2659 美丽的序列
题意:求整个序列的所有区间中,区间最小值×区间长度 的最大答案。
代码:正序维护单个元素不比它小最左可到哪L【i】,逆序维护单个元素不比它小最右可到哪R【i】,枚举每个元素,计算最大的a【i】× (R【i】-L【i】+1)即可。
ll n,m,k,q,a[maxn];
int l[maxn],r[maxn];
void init()
{
for(int i=1;i<=n;i++)
{
int st=i;
while(st>1 && a[st-1]>=a[i])
st=l[st-1];
l[i]=st;
}
for(int i=n;i>=1;i--)
{
int st=i;
while(st<n && a[st+1]>a[i])
st=r[st+1];
r[i]=st;
}
}
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
init();
ll ans=0;
for(int i=1;i<=n;i++)
{
ll val = a[i]*(r[i]-l[i]+1);
ans = max(ans,val);
}
cout<<ans<<endl;
}
单调栈练习题四:牛客-区区区间间间
题意:求整个序列所有区间的最大值减最小值总和。
代码:大体和上一题一样,第一遍跑出所有区间的最大值,a【i】全取负后再跑一遍即得到了所有区间的最小值,把所有最大值减去所有最小值即可。
ll n,m,k,q,a[maxn];
int l[maxn],r[maxn];
ll getpos()
{
for(int i=1;i<=n;i++)
{
int st=i;
while(st>1 && a[st-1]<=a[i])//<=
st=l[st-1];
l[i]=st;
}
for(int i=n;i>=1;i--)
{
int st=i;
//a[i+1]向左的时候l[i+1]会计算<=的情况,所以此处单取<
while(st<n && a[st+1]<a[i])
st=r[st+1];
r[i]=st;
}
ll ans=0;
for(int i=1;i<=n;i++)
{
ans += a[i]*(r[i]-l[i]);
ans += a[i]*(r[i]-i)*(i-l[i]);
}
return ans;
}
void solve()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
ll max_sum = getpos();
for(int i=1;i<=n;i++) a[i]=-a[i];
ll min_sum = getpos();
cout<<max_sum-(-min_sum)<<endl;
}
单调栈练习题五:洛谷-P3467 \POI2008\PLA-Postering
题意:只用矩形的海报来完全覆盖给出的若干个矩形,覆盖的海报不能超出任何一个给出的矩形,求最少使用的海报数量。
代码:需要一定的思维转换,结论是:默认情况下需要n张海报。所有矩形的宽度不需要考虑,只考虑高,然后维维护一个单调递减的栈(栈顶最大),若当前的矩形前面已经出现了,则这两张海报可以从底部连通,需要的海报数量减一。
ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
cin>>n;
ll w,ans=n;
for(int i=1;i<=n;i++)
{
cin>>w>>a[i];
while(stk.size() && stk.top()>=a[i])
{
if(a[i]==stk.top()) ans--;
stk.pop();
}
stk.push(a[i]);
}
cout<<ans<<endl;
}
单调栈练习题六:HDU1506-Largest Rectangle in a Histogram
参考博客
题意:在柱状图中寻找一个面积最大的矩形。
代码:维护一个单调递减的栈(栈顶最大),当添加的元素比栈顶小时,在不断弹出元素的过程中,因为我们单调栈存储的是下标,每个弹出元素与当前要插入的下标的差值即为矩形的宽,乘以下标对应元素的值即高即为当前元素可形成的矩形面积,更新出最大的答案。
1.在给定序列的最后插入一个值为-1的元素保证所有元素最终都会被弹出,以计算全局结果。
2.解释下其中关键的:a [ top ] = a [ i ] 这里对a进行直接修改的操作是为了优化,因维护的是单调递增栈且栈中存放的是下标,在后续计算这个新插入的元素的贡献时,top至i这一段被弹出的元素是虚拟存在的,并未完全消失,体现在我们计算矩形面积公式上: a [ top ] × ( i - top ) (建议手动模拟一下数据便于理解)
ll n,m,k,q,a[maxn];
stack<ll> stk;
void solve()
{
while(cin>>n)
{
if(!n) break;
for(int i=1;i<=n;i++) cin>>a[i];
a[n+1]=-1;
ll ans=0,top,val;
for(int i=1;i<=n+1;i++)
{
if(stk.empty() || a[stk.top()] <= a[i])
stk.push(i);
else
{
while(stk.size() && a[stk.top()]>a[i])
{
top = stk.top();
stk.pop();
val = a[top]*(i-top);
ans = max(ans,val);
}
stk.push(top);
a[top]=a[i];//
}
}
cout<<ans<<endl;
}
}
单调栈练习题七:牛客-删删删越小越好
给出长度最大2e7的一串数,要求删去k个位置上的数字,使结果串字典序最小。
维护一个单调递减的栈(栈顶最大),即按正序遍历下来,最后入栈的元素一定比前面的都大,否则弹出栈顶并删除弹出的下标对应的数字。
bool vis[21234567];
void solve()
{
string s;
int k;
cin>>s>>k;
int pos=0,len=s.size();
stack<int> stk;
while(k && pos<len)
{
while(stk.size() && s[stk.top()]>s[pos] && k)
stk.pop(),vis[stk.top()]=1,k--;
stk.push(pos++);
}
while(k--)
{
vis[stk.top()]=1;
stk.pop();
}
bool f=0;
for(int i=0;i<s.size();i++)
{
if(!vis[i])
{
if(s[i]=='0' && f)
cout<<s[i];
else if(s[i]!='0')
cout<<s[i],f=1;
}
}
if(!f) cout<<0<<endl;
}
§单调队列
Usage:单调队列,即单调递减或单调递增的队列,一般用于求指定区间内的最值问题
可认为是带容量的优先队列,但是容量指的不是元素个数,是按新旧程度衡量
1. 队列中的元素在原来的列表中的位置是由前往后的(随着循环顺序入队)。
2. 队列中元素的大小是单调递增或递减的。
ps:跟单调栈一样,一般也是存下标比较便捷。
//模板
int L=1,R=0;
for(int i=1;i<=n;i++)
{
//k容量的单调递减队列
while(L<=R && Q[L]+k<=i) L++;
while(L<=R && a[Q[R]]<=a[i]) R--;//根据情况改变比较符号
Q[++R]=i;
//记录状态答案
}
单调队列练习题一:洛谷-P2032 扫描
求区间最大,模板
ll n,m,k,q,a[maxn];
struct node
{
int id,v;
}Q[maxn];
int ans[maxn];
void solve()
{
read(n);read(k);
for(int i=1;i<=n;i++) read(a[i]);
int L=1,R=0;
for(int i=1;i<=n;i++)
{
while(L<=R && Q[L].id+k<=i) L++;
while(L<=R && Q[R].v<=a[i]) R--;
Q[++R].v=a[i];
Q[R].id=i;
if(i>=k) ans[i]=Q[L].v;
}
for(int i=k;i<=n;i++)
cout<<ans[i]<<'\n';
}
单调队列练习题二:洛谷-P1886 滑动窗口 /【模板】单调队列
求区间最小和最大,模板
ll n,m,k,q,a[maxn];
struct node
{
int v,id;
}Q[maxn];
int L,R;
void solve()
{
read(n);read(k);
for(int i=1;i<=n;i++) read(a[i]);
L=1,R=0;
for(int i=1;i<=n;i++)
{
while(L<=R && Q[L].id+k<=i) L++;
while(L<=R && Q[R].v>=a[i]) R--;
Q[++R].v=a[i];
Q[R].id=i;
if(i>=k) cout<<Q[L].v<<' ';
}
cout<<endl;
L=1,R=0;
for(int i=1;i<=n;i++)
{
while(L<=R && Q[L].id+k<=i) L++;
while(L<=R && Q[R].v<=a[i]) R--;
Q[++R].v=a[i];
Q[R].id=i;
if(i>=k) cout<<Q[L].v<<' ';
}
}
单调队列练习题三:洛谷-P1714 切蛋糕
题目要求是取连续的区间,区间长度最小1,最大k,使取出来的区间和最大化。
前缀和处理一下是必须的,然后维护一个单调递增(容量为k)的队列,因队列是单调递增,队列首是最小的那个,每次判断当前位置的前缀和减去队列首的前缀和是否会更大即可。
ll n,m,k,q,a[maxn];
ll sum[maxn];
deque<int> Q;
void solve()
{
read(n);read(m);
for(int i=1;i<=n;i++) read(a[i]);
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+a[i];
ll ans=-INF;
Q.push_back(0);
for(int i=1;i<=n;i++)
{
while(Q.front()+m<i)
Q.pop_front();
while(Q.size() && sum[Q.back()]>=sum[i])
Q.pop_back();
ans = max(ans,sum[i]-sum[Q.front()]);
Q.push_back(i);
}
cout<<ans<<endl;
}
单调队列优化DP一:洛谷-P1725 琪露诺
从i可到达【i+L,i+R】,那么对于i来说,可从【i-R,i-L】过来。
因起点为0,则下标小于L的点不可到达,枚举【L,n】的情况,更新一个单调递减队列,队列头下标比i-R还小的弹出,队列尾下标对应的dp值比dp【i-L】还小的弹出,队列头下标对应的就是【i-R,i-L】这一区间内的最大dp值,
状态转移方程为dp【i】= a【i】+ dp【Q.front()】。
注意,维护进栈的时候,需要判断进栈的这个点是否可达。
手写单调队列:
ll n,m,k,q,a[maxn];
int dp[maxn],Q[maxn];
void solve()
{
int l,r;
cin>>n>>l>>r;
for(int i=0;i<=n;i++)
{
cin>>a[i];
if(i&&i<l) dp[i]=-INF;
}
int s=1,t=0;
int ans=-INF;
for(int i=l;i<=n;i++)
{
while(s<=t && Q[s]<i-r) s++;
while(s<=t && dp[Q[t]]<=dp[i-l]) t--;
Q[++t]=i-l;
dp[i]=a[i]+dp[Q[s]];
if(i>n-r) ans=max(ans,dp[i]);
}
cout<<ans<<endl;
}
STL-deque实现:
ll n,m,k,q,a[maxn];
int dp[maxn],vis[maxn];
deque<int> Q;
void solve()
{
int L,R;
read(n);read(L);read(R);
for(int i=0;i<=n;i++) read(a[i]);
//i -> [i+L,i+R]
//== [i-R,i-L] -> i
vis[0]=1;
int ans=-INF;
for(int i=L;i<=n;i++)
{
while(Q.size() && Q.front()<i-R) Q.pop_front();
if(vis[i-L])
{
while(Q.size() && dp[Q.back()]<=dp[i-L]) Q.pop_back();
Q.push_back(i-L);
}
if(Q.size())
{
dp[i] = a[i]+dp[Q.front()];
vis[i]=1;
if(i>n-R) ans=max(ans,dp[i]);
}
}
cout<<ans<<endl;
}