单调栈、单调队列

单调队列与单调栈作用:

以下内容引用自该博客:单调队列与单调栈作用

单调栈解决的是以某个值为最小(最大)值的最大区间,实现方法是:求最小值(最大值)的最大区间,维护一个递增(递减)的栈,当遇到一个比栈顶小的值的时候开始弹栈,弹栈停止的位置到这个值的区间即为此值左边的最大区间;同时,当一个值被弹掉的时候也就意味着比它更小(更大)的值来了,也可以计算被弹掉的值得右边的最大区间。

下面给出单调栈的类型及它所对应的求的东西:
单调不升:该区间左边的数严格小于它,右边的数小于等于它
单调不降:该区间左边的数严格大于它,右边的数大于等于它
单调上升:该区间左边的数大于等于它,右边的数严格大于它
单调下降:该区间左边的数小于等于它,右边的数严格小于它
~~
单调队列解决的是区间最小(最大)值,实现方法是:求区间最小(最大)值,就维护一个递增的双端队列,队中保存原始序列的标号,当即将入队的元素的值比队尾的元素的值小(大)的时候就不断弹掉队尾,知道出现比它更小的值,当即将入队的元素队首元素的跨度(即将入队元素的序号到队首元素序列的区间)大于规定区间时就不断弹掉队首,直到跨度小于或等于所规定的区间。如此可保证队首元素为最小(最大)值,(但不能保证队尾就是原始序列中的最大(最小)值),并维护区间长度。
单调不降(或上升):区间最小值
单调不升(或下降):区间最大值

单调队列和单调栈的应用

(该处引用自单调队列和单调栈详解
单调队列:
可以查询区间最值(不能维护区间k大,因为队列中很有可能没有k个元素)
优化DP(见 https://blog.csdn.net/dacc123/article/details/50545577
单调队列一般是用于优化动态规划方面问题的一种特殊数据结构,且多数情况是与定长连续子区间问题相关联。

单调栈:
对于某个元素i:

  • 左边区间第一个比它小的数,第一个比它大的数
  • 确定这个元素是否是区间最值
  • 右边区间第一个大于它的值
  • 到右边区间第一个大于它的值的距离
  • 确定以该元素为最值的最长区间【 以自己为最小或最大值找到最大的区间,(对应 单调递增/单调递减)】;

【两者维护的时间复杂度都是O(n),因为每个元素都只操作一次。】

下面放两道单调队列和单调栈的相关题目:

题目:最大子序和(单调队列例题)

(时间限制:1000ms)
一个长度为n的整数序列,从中找出一段不超过m的连续子序列,使得整个序列的和最大。
例如: 1, -3, 5, 1, -2, 3
当m=4时,sum = 5+1-2+3 = 7
当m=2或m=3时,sum = 5+1 = 6

Input
多测试用例,每个测试用例:
第一行是两个正数n, m ( n, m ≤ 300000 )
第二行是n个整数

Output
每个测试用例输出一行:一个正整数,表示这n个数的最大子序和长度

Sample Input
6 4
1 -3 5 1 -2 3

Sample Output
7

题解:

这是一道单调队列模板题,把寻找子段和最大的问题转化为找前缀和之差最大的问题。(如果单纯的用dp做会超时)

【本题我们要找一个不超过m长度的和最大的子段(可以不取)。那么直接把所有前缀和求出来,For一遍每一个右端点,用单调队列维护一下前面m个以内最小的前缀和是多少就行了】

  • 区间和→两个前缀和相减

  • 求出s[i]表示序列前i项的和,则区间[i,j]中数的和=s[j]-s[i-1]。

  • 问题变为找出两个位置i,j,使得s[j]-s[i]最大并且j-i<=m。

  • 枚举右端点i,若i固定,问题变为找到j∈[i-m,i-1]使得s[j]最小。若k<j<i,并且s[k]>=s[j],则在i及i之后的扫描中,k永远不会成为最优决策。

  • 可以维护一个下标位置递增、对应前缀和的值递增的单调递增的队列,当 i 变化时及时判断队头是否超出m的范围,取队头为最优解(此时队头一定是最小),然后在队尾插入新的 i 并维护单调性,时间复杂度O(N)。

/*
结果:Accepted 运行时间:524ms 使用内存:2648KB
*/

#include <cstdio>
#include <algorithm>
using namespace std;
int sum[300010];
int head,tail,ans;

struct node {
    int val;///存该下标对应的前缀和
    int xb;///存下标
}que[300010];

int main( )
{
    int n, m, x;
    while( ~scanf( "%d%d", &n, &m ) ) 
    {
        head = 1,tail = 0;
        ans = 0;
        /// 读数据并计算前缀和
        for ( int i=1; i<=n; i++ ){
            scanf( "%d", &x );
            sum[i] = sum[i-1] + x;
        }
        /// 初始化队列
        /// 从2开始循环,所以ans初值赋为sum[1]
        que[++tail].val = sum[1];
        que[tail].xb = 1;
        ans = sum[1];

        for ( int i=2; i<=n; i++ ) {
            while ( que[tail].val >= sum[i] && tail >= head ) /// 维护单调性(单调递增,此时队头永远是最小的一个)
                tail--;
            que[++tail].xb = i;
            que[tail].val = sum[i];
            if ( i-m > que[head].xb )///确保不超过m长度
                head++;
            ans = max( ans, sum[i]-que[head].val );
        }
        printf( "%d\n", ans );
    }
    return 0;
}

/*不用结构,队列que中存的是下标*/
#include<cstdio>
#include<cstring>
#include<algorithm>
int que[300005],sum[300005];
using namespace std;
int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        int a;
        for(int i=1;i<=n;i++){
            scanf("%d",&a);
            sum[i]=sum[i-1]+a;
        }
        int head=1,tail=1;
        int ans=0;
        for(int i = 1; i <= n; i++){
            if(que[head] < i-m)
                head++;
            ans=max(ans,sum[i]-sum[que[head]]);
            while(head<=tail&&sum[que[tail]]>=sum[i])
                tail--;
            que[++tail] = i;
        }
        printf("%d\n",ans);
    }
    return 0;
}

题目:Largest Rectangle in a Histogram(单调栈例题)

在这里插入图片描述
Input
The input contains several test cases. Each test case describes a histogram and starts with an integer n, denoting the number of rectangles it is composed of. You may assume that 1 <= n <= 100000. Then follow n integers h1, …, hn, where 0 <= hi <= 1000000000. These numbers denote the heights of the rectangles of the histogram in left-to-right order. The width of each rectangle is 1. A zero follows the input for the last test case.

Output
For each test case output on a single line the area of the largest rectangle in the specified histogram. Remember that this rectangle must be aligned at the common base line.

Sample Input
7 2 1 4 5 1 3 3
4 1000 1000 1000 1000
8 1 5 4 8 6 7 3 2
0

Sample Output
8
4000
20

题解:

以每一个矩形的高度作为最终大矩形的高度,看最宽能是多少,然后统计最优解。

(暴力法O(n^2)超时)

用单调递减栈做(即从栈顶到栈底是呈单调递减的)。维护一个单调栈,在维护单调性的弹出操作时统计宽度,不断更新答案即可得到最优解。

(链接:http://acm.hdu.edu.cn/showproblem.php?pid=1506 )

/*AC 109ms*/
#include<cstdio>
#include<algorithm>
#include<cstring>
typedef long long ll;
const int MAX=1e5+5;
using namespace std;
ll h[MAX],w[MAX],stack[MAX];
int main()
{
    int n;
    while(scanf("%d",&n)&&n)
    {
        memset(h,0,sizeof(h));//多测试用例,必须要重新初始化
        ll ans=0,k;///k为右宽
        for(int i=1;i<=n;i++){
            scanf("%lld",&h[i]);
        }
        
        h[n+1]=0;//为了方便把最后剩下的单调递增的矩形也统计进去,我们假设h[n+1]的位置有一个高度为0的矩形,最后将它加入单调栈时他会将所有矩形都弹出,那么答案也就完成最后的更新了。
        int top=0;
        stack[0]=-1;///注意这里要将单调栈的0下标位置初始化为-1,否则有可能会在下面while处陷入死循环导致超时
        
        for(int i=1;i<=n+1;i++){
            if(h[i]>stack[top]){///严格单调递减
                stack[++top]=h[i];///入栈
                w[top]=1;//左宽为1,即该入栈元素本身
            }
            else{
                k=0;///第一个出栈元素右宽为0
                while(h[i]<=stack[top]){
                    w[top]+=k;///算出该出栈元素的总宽(左宽+右宽)
                    ans=max(stack[top]*w[top],ans);///计算以该出栈元素为高的矩形面积,更新最优解
                    k=w[top];///下一个出栈元素的右宽为上一个出栈元素的总宽
                    top--;
                }
                stack[++top]=h[i];///入栈
                w[top]=1+k;//该入栈元素左宽为最后一个出栈元素的总宽+1
            }
        }
        printf("%lld\n",ans);
    }
    return 0;
}

【单调栈】hdu1506 Largest Rectangle in a Histogram ----简单了解单调栈

题目:Feel Good

题目大意: 给出正整数n和一个(1 <= n <= 100 000)长度的数列,要求找出一个子区间,使这个子区间的数字和乘上子区间中的最小值最大。输出这个最大值与区间的两个端点。

题解:前缀和+单调栈
(做法和上面那题基本一样,只不过这道题的单调栈中存的是下标)

#include<cstdio>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=1e5+5;
ll a[N],stack[N],sum[N],zz[N];
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n+1;i++){
        if(i<=n)
            scanf("%lld",&a[i]);
        else
            a[n+1]=0;
        sum[i]=sum[i-1]+a[i];
    }
    int top=0;
    ll ans=0,tt=-1,r,l;
    a[0]=-1;
    for(int j=1;j<=n+1;j++){
        if(a[j]>a[stack[top]]){
            stack[++top]=j;
            zz[top]=1;///左边
        }
        else{
            ll k=0;///右边
            while(a[j]<=a[stack[top]]){
                ll q=zz[top]+k;
                ans=max(ans,a[stack[top]]*(sum[stack[top]+k]-sum[stack[top]-zz[top]]));
                if(ans>tt)
                    r=stack[top]+k,l=stack[top]-zz[top]+1;
                k=q;
                tt=ans;
                top--;
            }
            stack[++top]=j;
            zz[top]=k+1;
        }

    }
    printf("%lld\n",ans);
    printf("%lld %lld\n",l,r);
    return 0;
}

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值