Codeforces 1795E Explosions?

题目链接:Problem - E - Codeforces

题意:

给定一个整数序列,你可以选择其中一个元素-1,请问操作多少次能形成一个先严格递增后严格递减的序列,求出(操作次数+最大元素)的最小值

题解:

我第一感觉是二分最大元素的位置,实际上这个问题并没有二分性,最大元素的大小和(操作次数+最大元素)的值无关,所以假了。

但是思考过程中会发现,如果确定了最大元素,那么对元素左边和元素右边的操作实际上是一样的,如果能够低复杂度的计算操作次数,就可以直接得到答案。

通过几组数据的模拟,我们大致可以感觉到一种继承、递推的关系,以a[i]为最大元素和以a[i-1]为最大元素对应的操作次数存在一定的递推关系,我们可以从1跑到n取记录以a[i]对应的最小操作次数cnt[i],前文说了,元素左边和右边的操作是一样的,我们不妨先从左往右记录左边的cnt1:

当a[i]>a[i-1]时,cnt1[i]=cnt1[i-1],这是显然的。

当a[i]<a[i-1]时,我们怎么处理呢?朴素的思路是从i-1遍历到1,依次修改并记录操作次数,但是我们可以发现,如果我进行了操作,那么就会产生严格递减的子序列,而严格递减的连续子序列两个元素之间相差1,因为他是公差已知的等差序列,我们可以用长度和最大值来描述整个连续子序列,只需要记录最大值和长度,也就是当前点和区间长度就可以描述这个区间,其他冗余的点我们都可以去掉。

对这个连续子序列我们可以利用数学进行O(1)计算操作次数,根本不用回去遍历,他的操作次数就为len*(a[i]-a[i-1]),然后我们去除冗余的点,只记录a[i]和区间长度,之后我们要计算区间第一个数是否严格大于上一个数,也就是说是否要继续修改上一个区间,如果不严格大于的话,重复执行上述操作。如果出现第一项元素为负数的情况,证明我们减过头了,我们更新区间长度和操作次数即可。

然后我们把数组倒置再做一遍,得到数组L,R。Li,Ri分别表示将前(后)i个元素变成升序所需要的最小操作次数,遍历一遍取最值就是答案惹。

那么我们回过头来看看,把元素分段看成一段一段递减的子序列,现在需要维护的是连续子序列的最大值和长度,所以说有些点我们不用存储,我们只要存储最大的点和长度即可,我们怎么去除连续子序列冗余的点呢?很容易想到pop操作,我们用单调栈模拟这个问题就可以了

话不多说,上代码!

AC CODE:

#include<iostream>
#include<cmath>
#include<algorithm>
#include<map>
#include<vector>
#include<cstring>
#include<set>
#include<queue>
#include<iomanip>
#define eol "\n"
typedef long long ll;
typedef unsigned long long ull;
const int inf = 0x3f3f3f3f;
const int mod = 1e9+7;
const int N   = 2E6 + 7;
using namespace std;
ll ksm(ll a,ll b) {ll res=1;a%=mod; while(b){if(b&1)res=res*a%mod;b>>=1;a=a*a%mod;}return res%mod;}
ll gcd(ll a,ll b) {return b==0?a:gcd(b,a%b);}
ll lcm(ll a,ll b) {return a/gcd(a,b)*b;}
ll inv(ll x) {return ksm(x, mod-2);}
void solve(){
    ll n;cin>>n;
    vector<vector<ll>>v(3,vector<ll>(n+1));//v[1]存原序列,v[2]存翻转后的序列
    for(int i=1;i<=n;i++){
        cin>>v[1][i];
        v[2][n-i+1]=v[1][i];
    }
    vector<vector<pair<ll,ll>>>stack(3);//同样的东西开两份
    vector<vector<ll>>ans(3,vector<ll>(n+1));//同样的东西开两份
    ll op[3]={0,0,0};//同样的东西开两份
    for(int k=1;k<=2;k++) {//做两遍,一遍得出L数组,一遍得出R数组,k是当前在做第k份
        for (int i = 1; i <= n; i++) {
            ll len=1;
            while(!stack[k].empty()&&v[k][i] - len+1<=stack[k].back().first){//如果说我的
                op[k]+=( stack[k].back().first - v[k][i] + len)*stack[k].back().second;//更新操作次数
                len+=stack[k].back().second;//更新区间长度
                stack[k].pop_back();//pop冗余的元素
            }
            if(v[k][i] - len + 1<0){//首项<0,减过头了
                op[k]-=(len-v[k][i]-1)*(len-v[k][i])>>1;//等差求和算多减了多少给他加回去
                len=v[k][i];//更新区间长度
            }
            ans[k][i]=op[k];//记录Li(Ri)
            stack[k].push_back({v[k][i],len});
        }
    }
    ll ansv=1e18;
    for(int i=1;i<=n;i++)
        ansv=min(ansv,ans[1][i]+v[1][i]+ans[2][n-i+1]);//求最值
    cout<<ansv<<eol;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0),cout.tie(0);
    int t=1;cin>>t;
    while(t--)
        solve();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值