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

以前感觉单调队列和单调栈很强,但是一直没有具体花时间来学,等于是来补锅了
单调队列其实本质就是一个普通的队列,和优先队列还是有区别的,叫单调队列的原因是用这个普通的队列来维护一些单调增加或者单调减少的区间。
引用一下思想:就是在决策集合(队列)中及时排除一定不是最优的选择。
例题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;
}

猜你喜欢

转载自blog.csdn.net/qq_43402296/article/details/105227922