2022.08.13 提高组模拟赛 题解

· · 个人记录

前言

被 @Hooch @StayAlone @L_h_ 踩爆了。打得相当失败。本场我总分 440/500。@Hooch 和 @L_h_ AK 了。

T1

拼接 n 个只包含 sh 的字符串,使得最终字符串中 sh 子序列最多。

\rm Difficulty: 3

做法很精妙,很有思维难度。场切 6/42 人。

首先显然有一个想法就是把字符串都排序,然后顺着取就行了,怎么排序是重要的问题。

观察到确定两个字符串谁前谁后只需要确定谁的贡献更大即可,所以直接重载小于运算符,比较贡献大小即可。

这题我认为很经典,但是又有思维难度。

参考代码:

int n;
string ans;
int cnt,sum;
struct node{
    string s;
    int sn,hn;
    bool operator<(const node &p) const{
        return p.sn*hn<sn*p.hn;
    }//重载运算符比较贡献
} s[N];
signed main(){
    cin>>n;
    fr1(i,1,n){
        cin>>s[i].s;
        fr1(j,0,s[i].s.length()-1){
            s[i].hn+=(s[i].s[j]=='h');
            s[i].sn+=(s[i].s[j]=='s');
        }
    }
    sort(s+1,s+n+1);//排序
    fr1(i,1,n){
        ans+=s[i].s;
    }
    fr1(i,0,ans.length()-1){
        if(ans[i]=='h'){
            sum+=cnt;
        }
        else{
            cnt++;
        }
    }//统计答案
    cout<<sum<<endl;
    ET;
}

T2

给你 n 个名字和权值,输出每次对权值按从大到小选择排序后的整个名字序列。如果出现相同权值,按目前的先后顺序排列。

n\leqslant 20
\rm Difficulty:1

签到题,可是 ZYH 连到都没签。原因是原题没有目前这两个字,导致歧义。场切 31/42

选择排序核心思想就是选择最大的那个然后和目前没有排好序的第一个交换即可。

所以参考代码如下,非常简单,瞎搞就可以了:

int n;
struct node{
    string name;
    int id,val;
} s[30];
int maxn=uinf,id;
int las;
signed main(){
    ios::sync_with_stdio(false);
    cin>>n;
    las=1;
    fr1(i,1,n){
        cin>>s[i].name>>s[i].val;
        s[i].id=i;
    }
    fr1(i,1,n-1){
        maxn=uinf;
//      id=inf;
        int who;
        fr1(j,las,n){
            maxn=max(maxn,s[j].val);//找到最大值
        }
        fr1(j,las,n){
            if(maxn==s[j].val){
//              id=s[j].id;
                who=j;
                break;//取目前第一个即可 
            }
        }
        swap(s[las],s[who]);//直接交换即可
        las++;//排好序的部分增加一
        fr1(j,1,n-1){
            cout<<s[j].name<<" ";
        }
        cout<<s[n].name<<endl;//本题卡格式
    }
    return 0;
}

T3

$n\leqslant 10^3$。
\rm Difficulty:1\sim1.4

拿到之后没有思路,所以不妨打个大暴力,不妨定轮数最大为 10^4

然后它……过了……

不光是数据水的原因,本题似乎可以证明轮数不会超过某个值。详见 L_h_ 的本场题解。

参考代码其实也就是普通暴力了。本题场切 $30/42$。 ``` int n; int a[1010]; int t[1010]; int main(){ ios::sync_with_stdio(false); while(1){ cin>>n; if(!n){ break; } fr1(i,1,n){ cin>>a[i]; } fr1(i,1,10000){ bool f=0; fr1(j,2,n){ if(a[j]!=a[j-1]){ f=1;//判定是否达成目标 break; } } if(!f){ cout<<i-1<<" "<<a[1]<<endl; break;//达成的话输出即可 } fr1(j,1,n){ t[j]=a[j]/2;//取出一半 } fr1(j,1,n){ if(j==1){//特判 a[j]=a[j]/2+t[n]; if(a[j]%2){ a[j]++; } continue; } a[j]=a[j]/2+t[j-1];//改变糖的数量 if(a[j]%2){//如果糖数是奇数就要多分发一个 a[j]++; } } } } return 0; } ``` ## T4 我认为本场比赛最好的一道题。真的很棒。 > 有 $n$ 个位置,每个位置上可以搭建最大 $m_i$ 高度的摩天大楼,最终搭建出来的摩天大楼高度序列 $a$ 必须满足:对于任意 $i<j<k$,不存在 $a_i>a_j<a_k$。找出最大摩天大楼总高度。 > $n\leqslant 5\cdot 10^5$。 $\rm Difficulty:3

最后二十分钟搞出来暴力,倒数四分钟的时候调通了正解然后极限翻盘(

本题场切 9/42

首先观察到答案序列 a 必然是单峰的或者是平的,我们下面强行将平的这种情况归算到单峰。至于为什么是单峰,是因为存在谷就不可能满足条件。此外还可能有单调不增或者不降,但是我们可以把它看作峰在位置 1n 的单峰。

得到单峰性质之后,我们当然需要枚举峰。容易发现枚举峰之后直接贪心地去取答案了,我们以峰右侧的单调不增为例:

于是我们很容易可以写出暴力代码,时间复杂度 O(n^2)

int n;
int a[500010];
int ans;
int maxn=LLONG_MIN;
int las;
signed main(){
    ios::sync_with_stdio(false);
    cin>>n;
    fr1(i,1,n){
        cin>>a[i];
    }
    fr1(i,1,n){
        ans=0;
        las=a[i];
        ans+=a[i];
        fr2(j,i-1,1){
            if(a[j]>las){
                ans+=las;
            }
            else{
                las=a[j];
                ans+=las;
            }
        }
        las=a[i];
        fr1(j,i+1,n){
            if(a[j]>las){
                ans+=las;
            }
            else{
                las=a[j];
                ans+=las;
            }
        }
        maxn=max(maxn,ans);
    }
    cout<<maxn<<endl;
    return 0;
}

接下来考虑优化,不妨考虑 DP。令 ldp_i 表示左侧单调不降序列在结尾为 i 时的答案,rdp_i 表示右侧单调不升序列在开头为 i 时的答案。于是容易有转移方程(下面都以 ldp 为例):

ldp_i=\begin{cases} ldp_{i-1}+m_i,&m_i\geqslant m_{i-1}\\ ldp_{pre_i}+(i-pre_i)\times m_i,&m_i<m_{i-1} \end{cases} 上面的第一个柿子很容易理解,显然这样做可以继续维护单调不降的性质。第二个则是为了取到这个位置我必须退回到第一个左侧小于它的位置,中间的部分都使用这个高度填入。显然中间一定不存在一个位置 $<m_i$,因此这个柿子是对的。 如果是右侧 $rdp$ 的处理,直接倒过来,求出第一个在它右侧小于它的位置记录在 $nex$ 中即可。 关于 $pre$ 和 $nex$ 的处理,推荐使用单调栈。 参考代码如下: ``` int n; int a[500010]; int ldp[500010]; int rdp[500010]; int ans; int maxn=LLONG_MIN; int las; stack <int> s; int pre[500010],nex[500010]; signed main(){ ios::sync_with_stdio(false); cin>>n; fr1(i,1,n){ cin>>a[i]; } // fr1(i,1,n){ // while(!s.empty()&&a[s.top()]>a[i]){ // pre[s.top()]=i; // s.pop(); // } // s.push(i); //// cout<<pre[i]<<endl; // } fr2(i,n,1){ while(!s.empty()&&a[s.top()]>a[i]){ pre[s.top()]=i; s.pop(); } s.push(i); } while(!s.empty()){ s.pop(); } fr1(i,1,n){ while(!s.empty()&&a[s.top()]>a[i]){ nex[s.top()]=i; s.pop(); } s.push(i); } fr1(i,1,n){ if(nex[i]==0){ nex[i]=n+1; } } fr1(i,1,n){ ans=ldp[i-1]; // ans+=a[i]; if(a[i]>=a[i-1]){ ans+=a[i]; } else{ ans=ldp[pre[i]]+(i-pre[i])*a[i]; } ldp[i]=ans; } fr2(i,n,1){ ans=rdp[i+1]; // ans+=a[i]; if(a[i]>=a[i+1]){ ans+=a[i]; } else{ ans=rdp[nex[i]]+(nex[i]-i)*a[i]; } rdp[i]=ans; } fr1(i,0,n){ // cout<<i<<":"<<ldp[i]<<","<<rdp[i]<<endl; maxn=max(maxn,ldp[i]+rdp[i+1]); } cout<<maxn<<endl; return 0; } ``` $\rm P.S.$ 这是为数不多的单调栈好题了,不典也不板而且很有思维水平。NOIO 的丹钓战也是一道不错的题,然而需要一些高级数据结构。本题的好处在于不需要高级数据结构,但是仍然很有思维难度。 ## T5 > 给你三个数 $x,t,k$,你每次可以对 $x$ 做操作:使它减去 $i(1\leqslant i\leqslant t)$;如果它是 $k$ 的倍数,将它除以 $k$。求出最小的次数,使得 $x$ 变为 $1$。 > 本题存在多测,$1\leqslant T\leqslant 20$。$0\leqslant t\leqslant 10^6,1\leqslant x,k\leqslant 10^6$。保证有解。 $\rm Difficulty:2\sim2.5

单调队列优化 DP 板子题。场切 21/42

首先观察到做除法很麻烦,显然很难贪心,所以考虑 DP,而且时间倒流,我们从 1 开始扩展,算出最少次数。

不妨考虑线性 DP。令 dp_i 表示扩展到数 i 的答案,那么有很显然的转移方程:

dp_i= \begin{cases} \min\limits_{1\leqslant j\leqslant t}\{dp_{i-j}\}+1,&i\not\equiv0\pmod k\\ \min(\min\limits_{1\leqslant j\leqslant t}\{dp_{i-j}\}+1,dp_{\frac{i}{k}}),&i\equiv0\pmod k \end{cases}

然后观察到第二个式子最外面那个 \min 可以直接当初值赋走拆掉,于是本质上两个式子毫无区别,里面的那个 \min 为了线性使用单调队列优化即可。如果用 \log 数据结构搞的话可能会相当卡常。

参考代码:

int T;
int dp[1000010];
deque <pii> q;
int x,k,t;
void solve(){
    cin>>x>>k>>t;
    while(!q.empty()){
        q.pop_front();
    }
    memset(dp,0x3f,sizeof dp);
    dp[1]=0;
    q.push_back(mp(1,0));
    fr1(i,2,x){
        if(i%k==0){
            dp[i]=dp[i/k]+1;
        }
        while(!q.empty()&&q.front().fi<i-t){
            q.pop_front();
        }
//      cout<<i<<"!"<<q.front().fi<<endl;
        dp[i]=min(dp[i],q.front().se+1);
        while(!q.empty()&&q.back().se>=dp[i]){
            q.pop_back();
        }
        q.push_back(mp(i,dp[i]));
//      cout<<i<<","<<dp[i]<<endl;
    }
    cout<<dp[x]<<endl;
}
int main(){
    cin>>T;
    while(T--){
        solve();
    }
    return 0;
}

总结

这次的题相比之前难度提高了很多,思维难度有些提升,让我这种思维水平低的无处存储(

恭喜 @L_h_ 五题总用时 \color{green}4:32:06 的情况下 AK,拿下 \rm rank1。@Hooch 总用时 \color{green}?:38:58 AK。可怜的 ZYH 总分 440/500 收场,总时间高达 \color{red}8:05:19,还打不过五年级的(((

ZYH 菜菜/kk