2022 年杭电多校第六场 Multiply 2 Divide 2

2022 年杭电多校第六场 Multiply 2 Divide 2

题意:

BXY的序列\(a\)长度为\(n\) \((1\leq n\leq10^5,1\leq a_i\leq10^5)\)

对于每个操作,他选择一个数字\(a_i(1\leq i\leq n)\)并将其更改为\(a_i*2\)\(\lfloor \frac{a_i}{2} \rfloor\)。BXY想知道将序列a更改为非下降序列的最少的操作数。

多测,数据最多五组

思路:

我们可以先考虑一个暴力出来。

\(f_i,_j,_k\)表示第\(i\)个数字除了\(j\)次,乘了\(k\)次之后,让前面不递减的最小次数

我们可以得到以下转移:\(f_{i+1},_{j'},_{k'}=f_i,_j,_k+j'+k'\),当且仅当\(⌊ \frac{a _{i + 1}}{2^{j′}} ⌋ {2^{k ′}} ≥⌊ \frac{a _{i }}{2^{J}} ⌋ {2^{k }}\)
可能你会疑问,为什么满足此条件便成立呢?不用考虑乘除影响吗?

易知,先乘(全乘)再除是负收益。那么我们可不可以先除一些再乘一些再除一些呢?当然也是不可以的,因为乘完之后再除收益又变成负的了。为了保证所以值合理运用就只能先全除后乘了。(例如a*2/2=a,本来一次都不用的,你非要用两次)

再判断状态数量。设\(M\)为其中最大值。第一维\(O(n)\),第二维\(O(logMlogM)\),第三位是\(O(30)\)(毕竟一个数的乘法次数不能超过30次,当然这只是预估,你可以开大一点,但是绝对不小于\(logM\)),时间复杂度无法接受,考虑优化

我们发现,这种序列会有两种可能的情况:
1.全部小于等于M

2.1-i都小于等于M,i+1-n大于M

那么,当这个\(a_{i+1}\)为那个第一个大于M的数时,它一定是除完后一直乘直到恰好大于M的第一个数

基于此,我们不妨设两个数组\(ga_i,_j,gb_i,_j\)

\(ga_i,_j\)为第\(i\)个数先除\(j\)次后让其变成第一个大于M的数,且保证其后面的数单调递增的最小次数,\(gb_i,_j\)为辅助变量,表示为这个第一个大于M的数的值

例如,当M=7,x=4时,\(gb_1,_1=8,ga_1,_1=3\),因为\(4/2=2,2*2=4,4*2=8\),三次

那么本题的答案便是这个序列两种情况的最小值,其实就做完了。

看到这你应该是蒙的,所以我们具体解释一下如何实现

先解决第一个问题:\(ga,gb\)的初始化。

先预处理出每个数除j次的值,且保证其从大到小

我们先处理出每个数本身,不考虑其他数。这时的\(ga_i,_j\)代表的是\(i\)这个数除j次之后使得其恰好大于\(M\)的最小次数 就拿样例来说,\(ga_1,_2=6,gb_1,_2=16\)

然后,倒序递推即可

for(int k=0;k<20&&gb[i+1][k];k++){
    if(gb[i+1][k]<gb[i][j])mx=min(mx,ga[i+1][k]+n-i);//后面比前面小 全体乘2保证单调性 
    else mx=min(mx,ga[i+1][k]);
}
ga[i][j]+=mx;

处理第二个问题:两个序列如何求最小值?

对于第一种情况:

flag=INF;
for(int j=0;j<g[i].size();j++){
    while(k<g[i-1].size()&&g[i-1][k].val<=g[i][j].val){//如果前面小于后面 
        flag=min(flag,g[i-1][k].times);//在j不动的情况下求一个最小值 
        k++;
    }
    g[i][j].times+=flag;
}

对于第二种情况:

while(k<g[i-1].size()&&g[i-1][k].val<=mx){//钦定第i(i>1)个数为那个第一个大于M的数 
    flag=min(flag,g[i-1][k].times);//所以前面只要小于M都可以,无所谓 
    k++;
}    for(int j=0;j<20&&gb[i][j];j++)ans=min(ans,ga[i][j]+flag);//前面的最小+预处理数组ga 

所以这道题就做完了

完整代码

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=1e5+10;
const int INF=2e17;
struct node{
    int val,times;
};
int t,n,mx;
int val[maxn];
int ga[maxn][20],gb[maxn][20];
vector<node>g[maxn];
signed main(){
    cin>>t;
    while(t--){
        scanf("%lld",&n);mx=-1;
        for(int i=1;i<=n;i++){
            g[i].clear();
            for(int j=0;j<20;j++){
                ga[i][j]=INF;
                gb[i][j]=0;
            }
        }
        for(int i=1;i<=n;i++)scanf("%lld",&val[i]),mx=max(mx,val[i]);//M的预处理
        for(int i=1;i<=n;i++){
            vector<int>d;
            d.clear();
            d.push_back(val[i]);
            while(d.back()>1)d.push_back(d.back()/2);//倒叙添加
            int cnt=d.size();
            g[i].push_back((node){0,cnt});//当然可以除成0
            for(int j=cnt-1;d.back()<=mx;j--){//j可以小于0 
                for(int k=cnt-1;k>=max(j,(int)0);k--){
                    //这里的g其实只求了它本身除j次后变成>M的最小次数
                    if(d[k]<=mx){
                        g[i].push_back((node){d[k],2*k-j});//存下那些小于M的数为第一种序列做准备
                        d[k]=d[k]*2;
                    }
                    if(d[k]>mx&&(!gb[i][k])){
                        gb[i][k]=d[k];
                        ga[i][k]=2*k-j+1;//最后一次需要单独计算,于是+1 
                    }
                }
            }
        }
        //g的前缀和求法
        for(int i=n-1;i>=1;i--){
            for(int j=0;j<20&&gb[i][j];j++){
                mx=INF;
                for(int k=0;k<20&&gb[i+1][k];k++){//前提是这个数除以k次后不为0
                    if(gb[i+1][k]<gb[i][j])mx=min(mx,ga[i+1][k]+n-i);//后面比前面小 全体乘2保证单调性 
                    else mx=min(mx,ga[i+1][k]);
                }
                ga[i][j]+=mx;
            }
        }
        int ans=INF;
        for(int i=2;i<=n;i++){
            int k=0,flag=INF;
            for(int j=0;j<g[i].size();j++){
                while(k<g[i-1].size()&&g[i-1][k].val<=g[i][j].val){//如果前面小于后面 
                    flag=min(flag,g[i-1][k].times);//在j不动的情况下求一个最小值 
                    k++;
                }
                g[i][j].times+=flag;
            }
            while(k<g[i-1].size()&&g[i-1][k].val<=mx){//钦定第i个数为那个第一个大于M的数 
                flag=min(flag,g[i-1][k].times);//所以前面只要小于M都可以,无所谓 
                k++;
            }
            for(int j=0;j<20&&gb[i][j];j++)ans=min(ans,ga[i][j]+flag);//前面的最小+预处理数组ga 
        }
        for(int i=0;i<g[n].size();i++){
            ans=min(ans,g[n][i].times);//对第一种序列的整体求最小
        }
        printf("%lld\n",ans);
    }
    return 0;
}

后记

DP暴力优化是一种常用的手段,除了四边形,斜率等之类的优化,我们还可以对于转移加上限制(当然这道题没有使用)以达到我们的目的或寻找序列性质,用辅助数组加速优化。

其实我感觉这道题有点根号分治那味

posted on 2022-11-15 23:30  C2023CYJ  阅读(213)  评论(1)    收藏  举报