Codeforces Round 873 (Div. 2) D1-D2 Range Sorting

传送门(Easy Version)
传送门(Hard Version)

比较难的一道题(rating 2000,2400),但是可以总结出不少东西。

思路(Easy Version)

不难发现如下两条性质:

  1. 在最优的情况下,所有的操作区间不会交叉。

  2. 如果在 [ l , r ] [l,r] [l,r]上,存在 k ∈ [ l , r ) k∈[l,r) k[l,r)使得 m a x [ l , k ] < m i n [ k + 1 , r ] max[l,k]<min[k+1,r] max[l,k]<min[k+1,r],那么这两段可以单独计算(这样就省去了 [ k , k + 1 ] [k,k+1] [k,k+1]的操作。

其实由这两个性质就可以把Easy Version干出来了,方法如下:

先计算出一个上限的操作数,也就是把每个区间的 r − l r-l rl加出来。这样计算的结果为 ∑ i = 1 n i ∗ ( i − 1 ) / 2 ] \sum_{i=1}^n i*(i-1)/2] i=1ni(i1)/2]

然后我们利用上面的性质2,将刚才的值减少,直至得出答案,specifically,
统计 a [ i ] a[i] a[i]作为上面所述 m i n [ k + 1 , r ] min[k+1,r] min[k+1,r]时,它能让多少对 [ l , r ] [l,r] [l,r]的的操作数减 1 1 1

所以我们只需要找到三个位置 x , y , z x,y,z x,y,z y y y i i i左边第一个满足 a [ y ] < a [ i ] a[y]<a[i] a[y]<a[i]的位置, x x x y y y左边第一个满足 a [ x ] > a [ i ] a[x]>a[i] a[x]>a[i]的位置, z z z i i i右边第一个满足 a [ z ] < a [ i ] a[z]<a[i] a[z]<a[i]的位置,则当 x < l ≤ y , i ≤ r < z x<l \leq y, i \leq r<z x<ly,ir<z时它会对答案产生贡献,故 a [ i ] a[i] a[i]产生的贡献值为 ( y − x ) ⋅ ( z − i ) (y-x)·(z-i) (yx)(zi)

这样Easy Version就搞定了。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=5005;
int T,n,a[N],ans;

void ini()
{
    ans=0;
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin>>T;
	while(T--)
    {
        ini();
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i],ans+=i*(i-1)/2;
       // cout<<ans<<'\n';
        for(int i=2;i<=n;i++)
        {
            int x=0,y=-1,z=n+1;
            for(int j=i-1;j>=1;j--)
                if(a[j]<a[i]) {y=j; break;}
            if(y==-1) continue;
            for(int j=y-1;j>=1;j--)
                if(a[j]>a[i]) {x=j; break;}
            for(int j=i+1;j<=n;j++)
                if(a[j]<a[i]) {z=j; break;}
           // cout<<i<<' '<<x<<' '<<y<<' '<<z<<'\n';
            ans-=(y-x)*(z-i);
        }
        cout<<ans<<'\n';
    }
	return 0;
}

现在进入Hard Version

其实解决D2的方法无外乎就是优化刚才的算法,我们发现在刚刚的过程当中求 x , y , z x,y,z x,y,z的过程太慢了,我们可以通过预处理(利用一些数据结构)的方法去维护,去提高运行效率。

如何搞出数组中某个数左(右)边离该数最近的小(大)于它的数

考虑使用单调栈,时间复杂度 O ( n ) O(n) O(n)

从左到右扫一遍数列,并维护一个从小到大的单调栈,对于 a [ i ] a[i] a[i],如果它大于栈顶元素,就把它直接塞到栈里,此时栈顶元素就是它左边离它最近的小于它的数。如果小于栈顶元素就将栈顶元素弹出,直到栈顶元素小于 a [ i ] a[i] a[i](或者栈空,也可以一开始塞一个下限元素),这时栈顶元素也是满足条件的元素,然后再把 a [ i ] a[i] a[i]塞进去。

这样在预处理过后 y y y z z z每次用 O ( 1 ) O(1) O(1)的时间就可求出。
还剩 x x x

利用ST表求出在某个位置左(右)边离该位置最近的小于某个数的数

讲的有点拗口,可以按本题求 x x x的过程理解。

以本题为例,先用ST表预处理整个数列,维护区间最大值。

在循环过程中,在y的左边找,从大区间到小区间逐渐逼近答案,先看大区间的最大值是不是小于 a [ i ] a[i] a[i],如果可以就跳过去,然后再往前跑小区间,这样就可以用 O ( l o g n ) O(logn) O(logn)的时间把所有的小于 a [ i ] a[i] a[i]的元素都跳过去,到达我们想要的 x x x

这样Hard Version也搞定了。

#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N=3*1e5+5;
int T,n,a[N],rmq[N][20],l[N],r[N],ans;
stack<int>s;

void pre()
{
    for(int i=1;i<=n;i++) rmq[i][0]=a[i];
    for(int j=1;j<=log2(n);j++)
    for(int i=(1<<j);i<=n;i++)
        rmq[i][j]=max(rmq[i][j-1],rmq[i-(1<<(j-1))][j-1]);
    while(s.size()) s.pop();
    s.push(0);
    for(int i=1;i<=n;i++)
    {
        while(a[s.top()]>a[i]) s.pop();
        l[i]=s.top();
        s.push(i);
    }
    while(s.size()) s.pop();
    s.push(n+1); a[n+1]=0;
    for(int i=n;i>=1;i--)
    {
        while(a[s.top()]>a[i]) s.pop();
        r[i]=s.top();
        s.push(i);
    }
}

signed main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	cin>>T;
	while(T--)
    {
        ans=0;
        cin>>n;
        for(int i=1;i<=n;i++) cin>>a[i],ans+=i*(i-1)/2;
        pre();
        for(int i=2;i<=n;i++)
        {
            if(!l[i]) continue;
            int x=l[i];
            for(int j=log2(n);j>=0;j--)
                if(x>=(1<<j)&&rmq[x][j]<a[i]) x-=(1<<j);
            ans-=(l[i]-x)*(r[i]-i);
        }
        cout<<ans<<'\n';
    }
	return 0;
}

一些总结与思考

1.初始化的问题
这题一开始我以为不需要太考虑初始化的问题,因为这题的操作都是赋值操作,可以覆盖掉上一次的值。但是在一些边界方面的操作,不初始化往往是致命的,会让你产生奇奇怪怪的错误,所以尤其要注意诸如 0 , n + 1 0,n+1 0,n+1之类的位置。

2.st的写法
和校赛的dp题异曲同工,都提醒我们dp不仅可以正着推也可以倒着推。st不仅存自己右边的区间,也可以存自己往左的区间,而在这题显然存左边的区间显然要更方便一些。

3.取stack的栈顶元素

栈空的时候不能用stack.top(),要注意判断,或者插入上下限元素。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值