算法:单调栈

栈:先进后出

最好是找一些简单的数据用纸和笔模拟一下具体过程,清清楚楚地表现出来,理解一个比较难懂的程序最快的方式是对照着一个程序一步一步将过程模拟出来,那么就很容易理解整个程序的意思了,不要光看程序代码,不动笔,这样可能会耗很多时间并且最后也没有理解程序。

另外,查找程序的错误也可以模拟

 进栈出栈模拟

 P4387验证栈序列 传送门

思路:先按照pushed序列一个一个进栈,然后当栈顶元素与poped序列第一个元素相等时,则出栈,然后再与poped序列第二个元素比较,保证出栈顺序与poped序列顺序一致,当所有pushed序列元素进栈过一次完毕之后,而且全部都已经出栈过一次了,则最后栈为空,则Yes,否则No。

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=2e5+5;
int a[N],b[N],stk[N],tt;
int main()
{
int i,j=1,n,q;
scanf("%d",&q);
while(q--){
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(i=1;i<=n;i++){
scanf("%d",&b[i]);
}
for(i=1;i<=n;i++){
stk[++tt]=a[i];
while(tt&&stk[tt]==b[j]){
tt--;
j++;
}
}
if(tt==0) printf("Yes\n");
else printf("No\n");
tt=0;
j=1;
}
return 0;
}

题型:找第i个元素a[i]左边距离最近且比a[i]小(大)的值(或下标)

找第i个元素a[i]右边距离最近且比a[i]小(大)的值(或下标)

重点:主要判断什么时候进栈,什么时候出栈

找左边比它小的第一个元素:假设栈里面已经排了很多元素,然后准备放入下一个元素,将准备放入的元素与栈顶比较,如果栈顶大于等于准备放入的元素,则在准备放入的元素进栈前,先让栈顶元素出栈,因为要找的是比它小的左边第一个元素,假设栈顶元素没出栈,当准备放入的元素进栈后,因为其比刚才的栈顶元素小,所以下一个准备放入的元素要找也只能找到刚才进栈的元素,不可能去找刚才的栈顶元素,所以刚才的栈顶元素没必要仍然放进栈中,所以让其出栈,因此当栈顶元素大于等于准备放入的元素时,则先出栈,再进栈。总的来说,找左边这一情况,栈里面排的元素是有可能成为要找的答案的集合,而根本不可能成为答案的元素早已经出栈。

每个元素最多出栈一次,入栈一次,则最多2*n次,时间复杂度为O(n),如果用双重嵌套循环遍历,时间复杂度为O(n^2)

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e5+5;
int stk[N],tt;
int main()
{
int n,i,x;
cin>>n;
for(i=0;i<n;i++){
cin>>x;
if(tt&&stk[tt]>=x){
tt--;
}
if(tt) cout<<stk[tt]<<" ";
else cout<<"0"<<" ";
stk[++tt]=x;
}
return 0;
}

 找左边比它大的第一个同理,只需将出栈条件由stk[tt]>=x改为stk[tt]<=x

 找右边比它大的第一个元素的下标:假设栈里面已经排了很多元素,然后栈顶元素与准备放入的元素比较,如果栈顶元素比即将放入的元素小,那么该栈顶元素所要找的下标即为要放入元素的下标,那么它已经找到自己要找的了,就没必要还在栈里了,则栈顶元素先出栈,再让刚才要放入的元素进栈,如果栈顶元素大于等于即将放入的元素,那么栈顶元素还没找到它要找的下标,则先待在栈里,一直等到找到了才出栈。总的来说,找右边这一情况,栈里面排的元素是还没找到其右边比它大(或小)的第一个下标的元素集合,已经找到的就没必要在栈里面待着了,早已经出栈了。

例1:单调栈(找右边第一个比它大的元素下标)

#include<iostream>
#include<algorithm>
using namespace std;
const int N=3e6+5;
int a[N];
int stk[N],tt;
int ans[N];
int main()
{
int n,i;
cin>>n;
for(i=1;i<=n;i++){
cin>>a[i];
}
for(i=1;i<=n;i++){
while(tt&&a[stk[tt]]<a[i]){
ans[stk[tt]]=i;
tt--;
}
stk[++tt]=i;
}
for(i=1;i<=n;i++)
cout<<ans[i]<<" ";
return 0;
}

例2:P1901发射站 传送门

#include<iostream>
#include<algorithm>
using namespace std;
const int N=2e6+5;
int a[N],b[N],q[N];
long long sum[N],max1;
int tt;
int work(int x)
{
while(tt&&a[q[tt]]<=a[x]) tt--;
sum[q[tt]]+=b[x];
q[++tt]=x;
}
int main()
{
int n,i;
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%d%d",&a[i],&b[i]);
}
for(i=1;i<=n;i++){
work(i);
}
tt=0;
for(i=n;i>=1;i--){
work(i);
}
for(i=1;i<=n;i++){
max1=max(sum[i],max1);
}
printf("%lld",max1);
return 0;
}

 思路:发出的能量只能被两边的最近的且比它高的发射站接收,也就是找它左边第一个大于它的,并且把能量给它以及找比它右边第一个大于它的数,并且把能量给它,然后从下标1到n,遍历,求所有能量中的最大值

单调栈轮两次就可以了,而找右边第一个大于它的数实际上不太行的通,因为被传输能量的下标知道,为x,而从哪个下标传来能量,我们是不知道的(tt是队尾,一直在变)(找左边第一个最大的,从下标x传出能量,而最后tt就是我们要找的第一个最大的,因此其下标是传入能量的下标,为stk[tt]),但是可以转换为从n到1找左边大于它的第一个数。因此可以只考虑单调栈找左边最大这一情况,写一个自定义函数,然后分别在1到n和n到1调用函数,当然,单调栈里应该储存数组的下标,因为当我们找到左边第一个大于它的数时,需要知道它的下标,这样才能将能量值加到那个左边第一个大于它的数下标对应的sum。 

例3:Mike and Feet  传送门

#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int N=2e5+5;
int a[N],stk[N],l[N],r[N],res[N],tt;
int main()
{
int n,i,len;
scanf("%d",&n);
for(i=1;i<=n;i++){
scanf("%d",&a[i]);
}
for(i=1;i<=n;i++){
while(tt&&a[stk[tt]]>=a[i]) tt--;
if(tt==0) l[i]=1;
else l[i]=stk[tt]+1;
stk[++tt]=i;
}
tt=0;
for(i=n;i>=1;i--){
while(tt&&a[stk[tt]]>=a[i]) tt--;
if(tt==0) r[i]=n;
else r[i]=stk[tt]-1;
stk[++tt]=i;
}
for(i=1;i<=n;i++){
len=r[i]-l[i]+1;
res[len]=max(res[len],a[i]);
}
for(i=n-1;i>=1;i--){
res[i]=max(res[i],res[i+1]);
}
for(i=1;i<=n;i++){
printf("%d ",res[i]);
}
return 0;
}

思路:对于每个元素找到左边和右边第一个大于这个元素的位置,比如是a[i]元素,利用单调栈找到a[i]左边第一个最大的下标x,找到a[i]右边第一个最大的下标y,那么以a[i]为最小值的最大区间为x+1到y-1(不包括端点的情况),具体区间表示为l[i]到r[i],而在这个区间里包含a[i]的更小区间里,a[i]也是最小的。(找最大是找不到这样的区间的,因为找的是第一个最大的而不是最后一个),len是该最大区间的长度,当区间大小为len时,因为对于size为x的group strength值为这个group中熊的最小的height值即res[len]=a[i],而对于x 求出最大的strength值,所以res[len]=max(res[len],a[i]),这样以a[i]为最小值的最大区间的strenge值就找到了,而以a[i]为最小值的更小区间也是能找到的,比如说res[1]=5,res[3]=4,res[7]=2,res[10]=1,则有最小值为1的长度为10的区间,那么可以在这个长度为10的区间里找到长度为9的也以a[i]为最小值的区间,也能找到长度为8的区间且也以a[i]为最小值的区间,也能找到以长度为7的且也以a[i]为最小值的区间,那么,从n往前遍历,则前面的res是肯定大于等于后面的res的,比如res[n-1]肯定大于等于res[n]。res[7]=2表示以2为最小值的区间长度最大为7,而一个一个遍历下来,没有一个以a[i]为最小值的最大区间长度为8和9,而包括长度8和9区间的只能是比它们大的长度为10的区间,值为res[10],只是因为长度为8和9的区间值为res[10],而值为res[10]的最大区间长度为10,大于了8和9,所以遍历下来,没有算出区间长度为8和9的值,因而res[8]=res[10],res[9]=res[10],其它没算出来的同理

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值