牛客 2.16 区区区间间间(单调栈)

在这里插入图片描述

输入描述:
第一行输入数据组数T
对于每组数据,第一行为一个整数n,表示序列长度
接下来一行有n个数,表示序列内的元素

输出描述:
对于每组数据,输出一个整数表示答案
示例1
输入
3
3
4 2 3
5
1 8 4 3 9
20
2 8 15 1 10 5 19 19 3 5 6 6 2 8 2 12 16 3 8 17
输出
5
57
2712
说明
对于一组测试数据的解释:
区间[1, 2]的贡献为:4 - 2 = 2
区间[1, 3]的贡献为:4 - 2 = 2
区间[2, 3]的贡献为:3 - 2 = 1
2 + 1 + 2 = 5.

备注:
不保证数据随机生成!


单调递增栈:单调递增栈就是从栈底到栈顶数据是从大到小
单调递减栈:单调递减栈就是从栈底到栈顶数据是从小到大


单调递增栈可以保证,对于当前元素来说,栈顶元素是第一个大于当前元素的数
单调递增栈伪代码:

insert x
while(!sta.empty() && sta.top()<x)
    sta.pop()
sta.push(x)

分析:

首先,问题可以转化为求

其中mx_ij是从i-j区间内的最大值,mn_ij是从i-j区间内的最小值

可以通过单调栈维护每个点作为最大值的区间,统计以该点为最大值的区间数量

以a[i]为区间最大值,向左维护到l[i],向右维护到r[i],即从l[i]-r[i]区间内a[i]都是最大值

数量的求法:

分两种情况:

①:a[i]作为端点,即从l[i]到r[i]内再选一点作为另一个端点,共有r[i]-l[i]种情况

②:a[i]作为区间中的一个点,即从l[i]到i内选一个左端点,i到r[i]内选一个右端点,共有(i-l[i])*(r[i]-i)种情况

两种情况求和乘上a[i]即可求出以a[i]为最大值的区间的和

最小值的和可以把a[i]=-a[i]即可求出最小值的和,与前面的负号正好抵消

参考原博客


细节部分
左端点:单调递增栈(找第一个大于ai的数,下标+1)
右端点:单调不减栈(找第一个大于等于ai的数,下标-1)
只要两个中有一个是不减的就可以,这么设计是为了处理连续出现相同数的情况如:
8 6 6 2 7
1 2 3 4 5
如果两个栈都单调递增(找第一个大于ai的数)
那么对于第一个6,最大区间为[2,4],当前下标为2,所有区间为[2,3],[2,4]
对于第二个6,最大区间为[2,4],当前下标为3,所有区间为[2,3],[3,4],[2,4]
可以发现[2,3],[2,4]这两个区间算了两遍,答案错误

如果左端点向左找第一个大于ai的数(单调递增栈),右端点找第一个大于等于ai的数(单调不减栈)
那么对于第一个6,最大区间为[2,2],当前下标为2,所有区间为null
对于第二个6,最大区间为[2,4],当前下标为3,所有区间为[2,3],[3,4],[2,4]细节部分
左端点:单调递增栈(找第一个大于ai的数,下标+1)
右端点:单调不减栈(找第一个大于等于ai的数,下标-1)
只要两个中有一个是不减的就可以,这么设计是为了处理连续出现相同数的情况如:
8 6 6 2 7
1 2 3 4 5
如果两个栈都单调递增(找第一个大于ai的数)
那么对于第一个6,最大区间为[2,4],当前下标为2,所有区间为[2,3],[2,4]
对于第二个6,最大区间为[2,4],当前下标为3,所有区间为[2,3],[3,4],[2,4]
可以发现[2,3],[2,4]这两个区间算了两遍,答案错误

如果左端点向左找第一个大于ai的数(单调递增栈),右端点找第一个大于等于ai的数(单调不减栈)
那么对于第一个6,最大区间为[2,2],当前下标为2,所有区间为null
对于第二个6,最大区间为[2,4],当前下标为3,所有区间为[2,3],[3,4],[2,4]
这样就避免了重复计算区间的情况。


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a[100005],l[100005],r[100005];
ll solve(ll n)
{
    memset(l,0,sizeof(l));
    memset(r,0,sizeof(r));
    stack<ll> s,t;
    //单调递增栈,站内元素从栈底到栈顶递减,也就是只有比栈顶元素小,才能进栈,当比栈顶元素大的时侯,要把大的元素弹栈,再让当前元素进栈,由此来维护一个单调栈
    //找a[i]元素对应的左区间
    //这里是维护了一个单调栈,因为进栈的元素是下标i,栈内的有序的其实是进栈元素i对应的a[i]的值,
    
    for(int i=1;i<=n;i++)
    {
        while(s.size()&&a[i]>=a[s.top()])//(1)
        {
            s.pop();
        }
        if(!s.size()) l[i]=1;//左边元素都比a[i]小
        else l[i]=s.top()+1;//否则左区间为栈顶值+1
        s.push(i);//更新单调递增栈
    }
    //单调递增栈,从a[n]读到a[1];
    //找a[i]元素对应的右区间
    for(int i=n;i>=1;i--)
    {
        while(t.size()&&a[i]>a[t.top()])//(2)等于时不pop
        //(1),(2)只能有一处等于
        {
            t.pop();
        }
        if(!t.size()) r[i]=n;
        else r[i]=t.top()-1;
        t.push(i);
    }
    ll ans=0;
    for(int i=1;i<=n;i++)
    {
        ans+=a[i]*(r[i]-l[i]+(i-l[i])*(r[i]-i));
    }
    return ans;

}
int main()
{
    ll t;
    cin>>t;
    while(t--)
    {
        ll n;
        cin>>n;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
        }
        ll ans=solve(n);
        for(int i=1;i<=n;i++) a[i]=-a[i];
        ans+=solve(n);
        cout<<ans<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值