题解:CF2053H Delicate Anti-monotonous Operations

· · 题解

本题本来可以出构造方案但是最后只要最小化步数,很良心吧!

就是样例给的有点少。

先特判初始不能操作和初始已经全部是 w 的情况。手玩一下,发现大多数情况都有最大值为 nw-1。可以发现 w=2 是一个特殊的情况,所有 1,2 的连续段在最优策略下都不会改变,因为不可能删掉两个连续段之间的间隔,最终最大值为 2n 减去 1 的段数,最小操作次数就是第一问答案减掉原来的和,因为每个 1 都要操作一次变成 2,一次只能操作一个。

注意到剩下的情况都有答案为 nw-1。考虑一个简单的构造:找到任意一个操作的起点,先把它扩展到边上,即每次把 a a b ... 改成 a b b ... 然后继续下去。此时得到一个边上的可操作点,我们按相同的方式拓展即可。例如当前的连续段 a a b,在 b\neq w 的时候我们直接把两个 a 改成 w,b 即可;否则可以先改成 w-1,w,然后把后两个 w 改成 w-1,w-2,然后把两个 w-1 改成 w,w-2。注意到此时用到了第三个数,所以 w=2 的时候不能如此操作。

这样已经构造出一个合法解,且可以发现一般来说都可以让最后的 w-1 放在末尾,除非后面有一个长的 w 连续段。考虑证明的话就是如果 w-1 在中间,需要把两个连续段向两边扩展再合并,分析发现操作次数远远更劣。我们考虑枚举它放在哪个边上,然后做法变成取离开头最近的连续段移动到边上,然后再扩展。这里有一个小细节,如果枚举到的一边上有若干个 w,可以将其视作连续段,也可以将其直接忽略。

接下来是一些小细节。

关于如何具体证明这是最小步数,其实手玩各个 case 的时候已经可以感受到了。更具体地,写个优秀的拍子就可以对拍证明了!优秀的暴力写法在文章最下面。

提供一个我花了两个小时写的代码。

#include<bits/stdc++.h>
using namespace std;
#define MOD         998244353
#define speMOD      2933256077ll
#define int         long long
#define pii         pair<int,int>
#define all(v)      v.begin(),v.end()
#define pb          push_back
#define REP(i,b,e)  for(int i=(b);i<(int)(e);++i)
#define over(x)     {cout<<(x)<<endl;return;}
#define lowbit(x)   ((x)&(-(x)))
#define cntbit(x)   __builtin_popcount(x)
#define deal(v)     sort(all(v));v.erase(unique(v.begin(),v.end()),v.end())
#define lbound(v,x) lower_bound(all(v),x)-v.begin()
int n,w,N;
int a[200005],b[200005];
int solve(){
    int x=0,ans=0;
    REP(i,1,n)if(b[i]==b[i-1]){
        x=i-1;break;
    }
    int m=n;while(b[m-1]==w-1)--m;//>=m 的都是 w
    //先把 same 推到最边上
    ans+=x;//x x+1 m
    int f=m<=x+2,g=0,h=0;
    if(m<=x)return 1e18;
    if(!f&&x){
        int len=x+2;
        while(len<n&&b[len]==w-1)++len;
        if(len==x+3)g=1;
        else g=0;
    }
    if(g){
        if(b[x-1]!=w-1)b[x+1]=w-1,b[x]=b[x-1],--x;
        else ans+=2,b[x+2]=b[x]? b[x]-1:1;
    }
    for(int i=x-1;i>=0;--i){
        if(!i&&b[i]==w-1){--ans;h=1;break;}
        if(b[i]==w-1)f=0;
        if(!f)b[i+2]=b[i]? b[i]-1:1;
        else b[i+2]=w-1;
        b[i+1]=b[i];
    }
    if(h)x=1;
    else{
        x=0;
        if(b[0]==w-1){while(x+2<n&&b[x+2]==b[x+1])++x;if(x+3<n&&b[x+2]==b[x+3])x+=2;}
    }
    //b[x]=b[x+1], 目标推进 b[x+2]
    while(x+1<m){
        if(x+2==m)return ++ans;
        if(b[x]==w-1&&b[x+1]==w-1&&b[x+2]==w-1){++x;continue;}
        if(b[x+2]!=w-1){
            b[x]=w-1;b[x+1]=b[x+2];++ans;++x;
            continue;
        }
        //9119
        // x  
        if(b[x+3]==w-1){
            if(x+4<n&&b[x+4]==w-1&&(x+5>=n||b[x+5]!=w-1)){
                //911999 911199 991199
                ans+=2;swap(b[x],b[x+2]);++x;
            }else{
                //91199 91188 99888 99988
                ans+=3;
                b[x]=b[x+1]=w-1;b[x+2]=b[x+3]=w-2;
                x+=2;
            }
        }else{
            //91191 92991 92211 99111 99911
            ans+=4;b[x]=b[x+1]=w-1;b[x+2]=b[x+3];
            x+=2;
        }
    }
    return ans;
}
int spesolve(){
    N=n;
    REP(i,0,n)b[i]=a[i];
    if(b[0]==w-1&&b[1]==w-1){
        int x=0,y=n-1;
        while(b[x]==w-1)++x;
        while(b[y]==w-1)--y;
        int f=0;
        REP(i,x+1,y+1)if(b[i]==b[i-1])f=1;
        if(!f){
            n=y+1;
        }else{
            f=0;
            REP(i,x+1,n)if(b[i]==b[i-1])f=1;
            if(f){
                int cur=solve();
                REP(i,0,n)b[i]=a[i];
                n-=x;
                REP(i,0,n)b[i]=b[i+x];
                return min(cur,solve());
            }
        }
    }
    return solve();
}
void Main() {
    cin>>n>>w;
    REP(i,0,n)cin>>a[i];
    if(w==2){
        int ans=0;
        REP(i,1,n){
            if(a[i]==1&&a[i-1]==1)a[i-1]=2,++ans;
        }
        int res=0;
        REP(i,0,n)res+=a[i];
        cout<<res<<' '<<ans<<endl;
        return;
    }
    bool f=1;int s=0;
    REP(i,1,n)if(a[i]==a[i-1])f=0;
    REP(i,0,n)s+=a[i];
    if(f||s>=n*w-1){
        cout<<s<<' '<<0<<endl;
        return;
    }
    cout<<n*w-1<<' ';
    if(w>4){
        int lst=-1,op=1;
        REP(i,0,n)if(a[i]<w-1){
            if(lst!=a[i])op^=3,lst=a[i];
            a[i]=op;
        }else a[i]-=w-4;
        w=4;
    }
    REP(i,0,n)--a[i];
    if(n==2)over(1)
    int cans=spesolve();n=N;
    reverse(a,a+n);
    cans=min(cans,spesolve());
    over(cans)
}
void TC() {
    int tc=1;
    cin>>tc;
    while(tc--){
        Main();
        cout.flush();
    }
}
signed main() {
    return cin.tie(0),cout.tie(0),ios::sync_with_stdio(0),TC(),0;
}

注意到本题细节超级多。我们考虑一个优秀的暴力方便我们对拍调试。注意到我们只关心 =w=w-1 和更小的数以及其中的相等关系。我们保留 w,w-1,把剩下的数标为 1,2,其中第一个数标为 1,后面的数根据“和前一个数是否相等”来决定是 12。这样可以得到一个 O(4^n\mathrm{poly}n) 做法,可以很方便的对拍!这个东西在上面的代码里也实现了。

实际上在特判掉很多 corner case 之后 w-1 也是不重要的,可以变成 w=3,但是需要更多特判细节,容易写错。时间复杂度 O(3^n\mathrm{poly}n)。具体实现可以用 3 进制数记录状态,用 bfs 实现最小步数。