单调栈/单调队列(小结)

以前感觉单调队列和单调栈很强,但是一直没有具体花时间来学,等于是来补锅了
单调队列其实本质就是一个普通的队列,和优先队列还是有区别的,叫单调队列的原因是用这个普通的队列来维护一些单调增加或者单调减少的区间。
引用一下思想:就是在决策集合(队列)中及时排除一定不是最优的选择。
例题1:P1886 滑动窗口
思路:我们用单调队列来记录两个东西,一个是这个数的位置,一个是这个数的数值,以区间最大值为例(区间最小值类似的操作)我们用队列头来记录当前满足要求的区间最大值,假设现在判断到第i个数,窗口长度为x,现在把第i个元素放入队列的尾部,因为我们现在找x区间最大值,所以比x小的元素就没有了任何价值,所以要弹出队列,队列里保持了数组下标递增,数组值也是递增。然后我们队列头就是当前最大值,我们现在要判断队列的头和我们现在的值是不是在一个x区间,不是我们就弹出队头,直到符合要求为止。输出队头就是答案。
每个数只会进队出队一次,复杂度O(N);

#include<stdio.h>
#include<bits/stdc++.h>
using namespace std;
struct zxc
{
    int id,val;
} a[1000006];
deque<zxc>ma;
deque<zxc>mi;
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i].val);
        a[i].id=i;
    }
    for(int i=1; i<=n; i++)
    {
        while(!mi.empty())
        {
            int v=mi.back().val;
            if(a[i].val<=v)
            {
                mi.pop_back();
            }
            else
            {
                break;
            }
        }
        mi.push_back(a[i]);
        while(!mi.empty())
        {
            if(i-mi.front().id>=k)
            {
                mi.pop_front();
            }
            else
            {
                break;
            }
        }
        if(i>=k)
        {
            printf("%d ",mi.front().val);
        }

    }
    printf("\n");
    for(int i=1; i<=n; i++)
    {
        while(!ma.empty())
        {
            int v=ma.back().val;
            if(a[i].val>=v)
            {
                ma.pop_back();
            }
            else
            {
                break;
            }
        }
        ma.push_back(a[i]);
        while(!ma.empty())
        {
            if(i-ma.front().id>=k)
            {
                ma.pop_front();
            }
            else
            {
                break;
            }
        }
        if(i>=k)
        {
            printf("%d ",ma.front().val);
        }

    }
    return 0;
}

例题2:P1440 求m区间内的最小值
和上一道题的求最小值类似,只不过这个不包括当前本身的,就是当前位置前m个(注意读题)

#include<stdio.h>
#include<bits/stdc++.h>
using namespace std;
struct zxc
{
    int id,val;
} a[2000006];
deque<zxc>ma;
deque<zxc>mi;
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i].val);
        a[i].id=i;
    }
    printf("0\n");
    for(int i=1; i<n; i++)
    {
        while(!mi.empty())
        {
            int v=mi.back().val;
            if(a[i].val<=v)
            {
                mi.pop_back();
            }
            else
            {
                break;
            }
        }
        mi.push_back(a[i]);
        while(!mi.empty())
        {
            if(i-mi.front().id>=k)
            {
                mi.pop_front();
            }
            else
            {
                break;
            }
        }
            printf("%d\n",mi.front().val);

    }
    return 0;
}

例题3:P1714 切蛋糕
这道题说小Z最多可以吃M块蛋糕,但是现在可以拿k<=M块,求k块和最大。但是数据存在负数,也就是说,小Z没有必要一定拿M块,所以就不能用尺取直接来了。
那我们可以先记录前缀和,然后假设当前我们判断位置为x,那么我们只要找到x的M-1个前缀和最小的,然后我们用当前x的前缀和减去这个最小的值,得到的自然就是最大的。

#include<stdio.h>
#include<bits/stdc++.h>
using namespace std;
struct zxc
{
    int id,val,sum;
} a[500006];
deque<zxc>ma;
deque<zxc>mi;
int main()
{
    int n,k;
    scanf("%d%d",&n,&k);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&a[i].val);
        a[i].id=i;
        a[i].sum+=a[i-1].sum+a[i].val;
    }
    int ans=0;
    for(int i=1; i<=n; i++)
    {
        while(!ma.empty())
        {
            int v=ma.back().sum;
            if(a[i].sum<v)
            {
                ma.pop_back();
            }
            else
            {
                break;
            }
        }
        ma.push_back(a[i]);
        while(!ma.empty())
        {
            if(i-ma.front().id>k)
            {
                ma.pop_front();
            }
            else
            {
                break;
            }
        }
        ans=max(a[i].sum-ma.front().sum,ans);
    }
    printf("%d\n",ans);
    return 0;
}

例题4:P1725 琪露诺
这道题一道DP,DP方程很好想,但是纯for循环的话,过不去,时间不允许。所以我们用单调队列去优化DP,在方程转移的时候,我们用单调队列来维护前一段范围dp的最大值,(注意这个队列里维护的不是给的数组,而是已经得出的当前位置L~R的一段DP的最大值)

#include<stdio.h>
#include<bits/stdc++.h>
using namespace std;
struct zxc
{
    long long  id,val,sum;
} dp[2000006];
long long  b[2000005];
deque<zxc>ma;
deque<zxc>mi;
int  main()
{
    long long  n,l,r;
    scanf("%lld%lld%lld",&n,&l,&r);
    for(long long  i=0; i<=n; i++)
    {
        scanf("%lld",&b[i]);
        dp[i].id=i;
       dp[i].val=0;
    }

    for(long long  i=0; i<=n+r-l+1; i++)
    {
        while(!ma.empty())
        {
            long long  v=ma.back().val;
            if(dp[i].val>=v)
            {
                ma.pop_back();
            }
            else
            {
                break;
            }
        }
        ma.push_back(dp[i]);
        while(!ma.empty())
        {
            if(i-ma.front().id>r)
            {
                ma.pop_front();
            }
            else
            {
                break;
            }
        }
        dp[i+l].val=ma.front().val+b[i+l];
    }
    long long  ans=-10000000000;
    for(int i=n+1;i<=n+r;i++)
    {
        ans=max(ans,dp[i].val);
    }
    printf("%lld\n",ans);
    return 0;
}

例题5:P2629 好消息,坏消息
也是记录前缀,把数组再复制一遍,然后滑动长度为n的窗口,找到区间最小值,然后减去这个窗口的开始地方的值,如果大于等于0,就记录一下,最后输出总个数即可。

#include<stdio.h>
#include<bits/stdc++.h>
using namespace std;
struct zxc
{
    long long  id,val,sum;
} a[2000006];
deque<zxc>ma;
int  main()
{
    long long  n;
    scanf("%lld",&n);
    a[0].sum=0;
    for(long long i=1; i<=n; i++)
    {
        scanf("%lld",&a[i].val);
        a[i+n].val=a[i].val;
    }
    for(long long  i=1; i<2*n; i++)
    {

        a[i].id=i;
        a[i].sum+=a[i-1].sum+a[i].val;
    }
    long long ans=0;
    for(long long  i=1; i<2*n; i++)
    {
        while(!ma.empty())
        {
            long long  v=ma.back().sum;
            if(a[i].sum<=v)
            {
                ma.pop_back();
            }
            else
            {
                break;
            }
        }
        ma.push_back(a[i]);
        while(!ma.empty())
        {
            if(i-ma.front().id>=n)
            {
                ma.pop_front();
            }
            else
            {
                break;
            }
        }
        if(i>=n&&(ma.front().sum-a[i-n].sum>=0))
        {
            ans++;
        }
    }
    printf("%lld\n",ans);
    return 0;
}

例题6:P2422 良好的感觉
这道题就是让你找以当前数为最小值的最大区间和,也就是找到比当前数小的左右两个端点。我们可以用单调栈维护一个递增的序列。假设当前进去一个数,如果这个数比栈顶元素小,那么栈顶元素出栈,那么这个数就是栈顶元素的右极限端点,那么栈顶元素的左极端端点就是当前栈的靠近栈底方向的下一个元素,因为这个栈是单调递增的,那么找到这两个的位置就找到了栈顶元素为最小值的最大区间,(这道题队列里是下标)还要注意记录前缀和,因为要找区间和。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<algorithm>
#include<stack>
using namespace std;
#define LL long long
const int N=1e5+10;
LL a[N];
LL sum[N];
stack<LL>q;
int main()
{
    LL n;
    scanf("%lld",&n);
    for(LL i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        sum[i]+=sum[i-1]+a[i];
    }
    a[n+1]=0;
    LL ans=0;
    a[0]=0;
    q.push(0);
    for(LL i=1;i<=n+1;i++)
    {
        while(!q.empty())
        {
            if(a[q.top()]>a[i])
            {
               LL x=q.top();
               q.pop();
                ans=max(ans,a[x]*(sum[i-1]-sum[q.top()]));
            }
            else
            {
                break;
            }
        }
        q.push(i);
    }
    printf("%lld\n",ans);
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Python中,单调栈单调队列是两种不同的数据结构单调栈是一个栈,它的特点是栈内的元素是单调的,可以是递增或递减的。在构建单调栈时,元素的插入和弹出都是在栈的一端进行的。与此类似,单调队列也是一个队列,它的特点是队列内的元素是单调的,可以是递增或递减的。在构建单调队列时,元素的插入是在队列的一端进行的,而弹出则是选择队列头进行的。 单调队列在解决某些问题时,能够提升效率。例如,滑动窗口最大值问题可以通过使用单调队列来解决。单调队列的结构可以通过以下代码来实现: ```python class MQueue: def __init__(self): self.queue = [] def push(self, value): while self.queue and self.queue[-1 < value: self.queue.pop(-1) self.queue.append(value) def pop(self): if self.queue: return self.queue.pop(0) ``` 上述代码定义了一个名为MQueue的类,它包含一个列表作为队列的存储结构。该类有两个方法,push和pop。push方法用于向队列中插入元素,它会删除队列尾部小于插入元素的所有元素,并将插入元素添加到队列尾部。pop方法用于弹出队列的头部元素。 总结来说,单调栈单调队列都是为了解决特定问题而设计的数据结构单调栈在构建时元素的插入和弹出都是在栈的一端进行的,而单调队列则是在队列的一端进行的。在Python中,可以通过自定义类来实现单调队列的功能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值