Skip to content

20231007 B 组模拟赛 题解

前言

这场我记得赛时切 T1T4,最终得分 100+45+15+100=260,排名贼高。这告诉我们正确的开题顺序有多重要。

密码是通用密码

T1

这道题很简单,考虑自己模拟一下。容易发现必然是先向一个方向走(下文以起始方向向右为例),遇到第一个 $L$ 回头,而之前经过的 $R$ 都变成 $L$ 了,那么向左遇到起点左侧第一个 $R$ 再回头,再往右的时候容易发现之前的 $L$, 就变成 $R$ 了,会碰到下一个 $L$ 再回头。那么我们只需要统计初始序列当前位置左侧有多少个 $R$,右侧有多少个 $L$ 就行了。具体计算很简单,我就不多赘述了。

复杂度可以 $O(n)$,取决于你维护的方式。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(i64 i=(s);i<=(e);i++)
#define fordown(i,s,e) for(i64 i=(s);i>=(e);i--)
using namespace std;
using i64=long long;
#define gc getchar()
inline i64 read(){
    i64 x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const i64 N=1e6+5;
i64 n,a[N],cntl[N],suml[N],sumr[N];
char str[N];
signed main(){
    n=read();
    scanf(" %s",str+1);
    forup(i,1,n){
        a[i]=(str[i]=='L');
    }
    fordown(i,n,1){
        cntl[i]=cntl[i+1];
        if(a[i]){
            ++cntl[i];
            suml[cntl[i]]=suml[cntl[i]-1]+i;
        }
    }
    i64 cntr=0;
    forup(i,1,n){
        if(cntr==cntl[i+1]){
            i64 ls=i*cntr-sumr[cntr],rs=suml[cntl[i+1]]-i*cntl[i+1],ans=ls*2+rs*2;
            if(a[i]){
                ans+=i;
            }else{
                ans+=n-i+1;
            }
            printf("%lld ",ans);
        }else if(a[i]){
            if(cntr<=cntl[i+1]){
                i64 ls=i*cntr-sumr[cntr],rs=(suml[cntl[i+1]]-suml[cntl[i+1]-cntr])-i*cntr;
                printf("%lld ",ls*2+rs*2+i);
            }else{
                i64 ls=i*(cntl[i+1]+1)-(sumr[cntr]-sumr[cntr-cntl[i+1]-1]),rs=suml[cntl[i+1]]-i*cntl[i+1];
                printf("%lld ",ls*2+rs*2+n+1-i);
            }
        }else{
            if(cntr>=cntl[i+1]){
                i64 rs=suml[cntl[i+1]]-i*cntl[i+1],ls=i*cntl[i+1]-(sumr[cntr]-sumr[cntr-cntl[i+1]]);
                printf("%lld ",ls*2+rs*2+n+1-i);
            }else{
                i64 rs=(suml[cntl[i+1]]-suml[cntl[i+1]-cntr-1])-i*(cntr+1),ls=i*cntr-sumr[cntr];
                printf("%lld ",ls*2+rs*2+i);
            }
        }
        if(!a[i]){
            ++cntr;
            sumr[cntr]=sumr[cntr-1]+i;
        }
    }
}

T2

首先,对于划分成 $k$ 段的答案,容易发现重排后必定是一个前缀贡献其 $0$ 的数量,一个后缀贡献其 $1$ 的数量,中间有至多一个贡献前半部分 $1$ 与后半部分 $0$。而容易想到是不是可以把这一段分成两半,然后算分成 $k+1$ 段,每一的段贡献就是 $\max(cnt_0,cnt_1)$。

为方便叙述,称贡献为 $cnt_0$ 的段为“零段”,贡献为 $cnt_1$ 的段为“壹段”(大写是为了防止歧义)。容易发现上面的猜想其实在 $k\le 2$ 的时候都是可以的。首先若存在某一段 $i$,使得原序列上第 $i$ 段是零段,第 $i+1$ 段是壹段,那么直接可以把这两段合在一起。否则必然原序列上前缀全是壹段,后缀全是零段,那么必然存在至少一个 $i$,使得 $i,i+1$ 类型相同。可以考虑把这两段合在一起。对于 $k=1$,直接暴力即可,方法很多,不多赘述。

另外,容易发现原序列 $s$ 上连续的 $0$ 和 $1$ 分不分开其实无所谓,或者说连续的同一个数可以当成一个固定不分开的块,不影响答案,对于大于等于块数的 $k$,容易发现答案就是 $n$(虽然其实这并不重要)。

那么问题就转化成了有一个 $01$ 交替的带权序列,你要把它分成 $k$ 段,每一段的权值是 $\max\left(\sum val_0,\sum val_1\right)$,求最大权值和。

而容易发现这其实是一个经典的反悔贪心,相当于最初所有段都做贡献,然后每次删掉一些段不做贡献,再把左右两段合并(比如最初是 $010$,假如合并成一个 $0$ 相当于把中间一个 $1$ 的贡献删掉了)。另外,容易发现每一对相邻的 $01$ 中必定有至少一个会产生贡献,即永远不会删除两个相邻的段。

套路地,这可以用优先队列 $+$ 双向链表维护,先不考虑边界,最初插入所有初始段,每次选出权值最小的一段 $x$ 删掉,然后把它前后两段 $px,nx$ 合并,合并的一段权值为 $val_{px}+val_{nx}-val_x$,下次如果删掉这段相当于删掉 $px,nx$ 再把 $x$ 重新加入贡献,由于每次选的都是最小的,它必定不会产生负数。合并后段数 $-2$,更新对应段数的答案。

容易发现边界不太好处理,我们可以分类讨论左右边界删掉/不删掉来解决。注意删掉边界只会使段数 $-1$,另外注意假如钦定边界删掉那么边界的下一个必定不会删掉,注意特判。

然后对答案求一个前缀最大值(因为中间可能有空缺),更新原问题答案即可,复杂度 $O(n\log n)$。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=3e5+5,inf=0x3f3f3f3f;
int n,a[N],ans[N],res[N],sz;
char str[N];
vector<int> vec;
int sv[N];
struct Node{
    int pos,val;
    bool operator <(const Node &r)const{return val>r.val;}
};
priority_queue<Node> q;
int ppcnt(int x){
    int res=0;
    while(x){
        x-=x&-x;
        ++res;
    }
    return res;
}
int pre[N],nxt[N];
int suf[N];
signed main(){
    n=read();
    scanf(" %s",str+1);
    forup(i,1,n){
        a[i]=(str[i]=='1');
    }
    int nw=1;
    forup(i,2,n){
        if(a[i]==a[i-1]){
            ++nw;
        }else{
            vec.push_back(nw);
            nw=1;
        }
    }
    vec.push_back(nw);
    sz=vec.size();
    forup(Case,0,3){// (2)!
        while(q.size()) q.pop();
        int cnt=ppcnt(Case),nw=vec[Case&1]+vec[sz-1-((Case>>1)&1)];
        mem(sv,-1);
        forup(j,1+(Case&1),sz-2-((Case>>1)&1)){
            sv[j]=vec[j];
            q.push(Node{j,sv[j]});
            nxt[j]=j+1;pre[j]=j-1;
            nw+=vec[j];
        }
        pre[1+(Case&1)]=nxt[sz-2-((Case>>1)&1)]=0;
        res[sz-cnt]=max(res[sz-cnt],nw);
        while(sz-cnt>=3){
            while(q.size()&&sv[q.top().pos]!=q.top().val) q.pop();
            Node u=q.top();q.pop();
            nw-=u.val;
            if(pre[u.pos]==0){
                sv[nxt[u.pos]]=sv[u.pos]=-1;
                cnt+=2;
                pre[nxt[nxt[u.pos]]]=0;
            }else if(nxt[u.pos]==0){
                sv[pre[u.pos]]=sv[u.pos]=-1;
                cnt+=2;
                nxt[pre[pre[u.pos]]]=0;
            }else{
                sv[pre[u.pos]]+=sv[nxt[u.pos]]-u.val;
                q.push(Node{pre[u.pos],sv[pre[u.pos]]});
                sv[u.pos]=sv[nxt[u.pos]]=-1;
                cnt+=2;
                int L=pre[u.pos],R=nxt[nxt[u.pos]];
                nxt[L]=R;pre[R]=L;
            }
            res[sz-cnt]=max(res[sz-cnt],nw);
        }
    }
    fordown(i,n,1){// (1)!
        suf[i]=suf[i+1]+a[i];
    }
    ans[1]=suf[1];
    int ss=0;
    forup(i,1,n){
        ss+=(a[i]==0);
        ans[1]=max(ans[1],ss+suf[i+1]);
    }
    forup(i,2,n){
        nw=max(nw,res[i+1]);
        ans[i]=nw;
    }
    forup(i,1,n){
        printf("%d ",ans[i]);
    }
}
  1. 暴力计算 $k=1$ 的答案。
  2. 分类讨论边界删不删掉。

T3

本场考试最难题。

设原序列为 $S$,分出来的两个序列为 $A,B$,$P(S)$ 表示 $S$ 序列中前缀最大值集合,$Q(S)=S-P(S)$。

首先,由于要求字典序最小的序列,我们可以考虑从前往后贪心地,能填 $0$ 就填 $0$,否则填 $1$。换句话说,我们希望求出在确定某一前缀后,后面是否存在某种分配方式使得 $A,B$ 序列中前缀最大值的个数相等。

首先可以想到设 $f_{i,a,b,s}$ 表示考虑后缀 $S_{i\sim n}$,$A$ 的下一个前缀最大值为 $a$,$B$ 的下一个前缀最大值为 $b$,$|P(A')-P(B')|=s$ 的后缀方案是否存在,这个可以很简单地 $O(n^4)$ 求出。但是很不好优化。考虑寻找性质。

容易发现,$P(A)\cup P(B)\supseteq P(S)$,这个比较显然,因为若 $S_i$ 为前缀最大值,无论怎么分,你显然不可能在 $S_{1\sim i-1}$ 之间找到任何一个大于 $S_i$ 的让它变成不是前缀最大值。那么我们其实更关心 $Q(S)$ 在 $P(A),P(B)$ 中的部分。

而我们又可以发现,必定可以构造出解使得 $Q(S)$ 仅与 $P(A),P(B)$ 其中之一(或者两者都不)有交。考虑若 $u,v\in Q(S),u\in P(A),v\in P(B)$,那么假如把 $u$ 放到 $B$ 中,$v$ 放到 $A$ 中,会使得 $u\in Q(A),v\in Q(B)$(因为 $S$ 中 $u$ 前面必定存在大于 $u$ 数,且被分到 $B$ 中了,$v$ 同理),$|P(A)-P(B)|$ 的值没变,但是两边的 $P$ 与 $Q(S)$ 的交都减小了 $1$,最终必定能使得其中一边与 $Q(S)$ 完全无交。

另外值得注意的是,我们只用这样的构造(指只有至多一个 $P$ 与 $Q(S)$ 有交)来判断后缀是否有解,并不影响答案的构造。(但是有趣的是如果就按这样构造答案仍能通过 CatOJ 以及 AtCoder 原题所有数据,但我不会证啊。等个有缘人)

因为上面的结论,若钦定 $P(A')\cap Q(S)=\varnothing$ 显然有 $|P(A')|+c_A=|SP(B')|+|SQ(B')|+c_B$,其中 $A',B'$ 表示当前后缀,$c_A,c_b$ 表示前面已有的前缀最大值个数,$SP(B')$ 表示 $P(B')\cap P(S)$,$SQ(B')$ 表示 $P(B')\cap Q(S)$。

然后移一下项,能得到 $c_A+c_B+|P(A')|+|SP(B')|=c_A+c_B+|P(S')|=2|SP(B')|+|SQ(B')|$。

容易发现 $c_a+c_b+|P(S')|$ 是已知的,也就是说只要能凑出 $2|SP(B')|+|SQ(B')|$ 等于某已知量就说明后缀有解。令 $x=|SP(B')|,y=|SQ(B')|$。

另外,容易发现若能凑出 $2x+y$,那么也能凑出 $2(x-1)+y$,因为上面的那个式子左边和 $A$ 完全没有关系,你可以吧 $SP(B')$ 里面的东西换到 $P(A')$ 里面,而如果 $x=0$,那么 $SQ(B')$ 中所有东西放到 $A$ 中都不会产生贡献,因为 $P(A)$ 中必定有比它们大的前缀最大值。这说明什么?说明奇偶性相同的数中,小于等于能凑出的最大值的数均可凑出来。

那么这就是一个带权最大上升子序列,可以用线段树维护当前后缀信息。并且由于 $S$ 是一个排列,那么线段树维护区间最大值其实就是可撤回的了(这么说有点抽象,可以看看代码)。

复杂度 $O(n\log n)$。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=2e5+5,NN=1<<18,inf=0x3f3f3f3f;
int n,p[N],ip[N],V;
struct SegTree{//这就是 zkw 线段树带给我的自信
    int querymax[NN<<1];
    void Build(){
        mem(querymax,0xc0);
    }
    void Update(int P,int X){
        querymax[P+NN]=X;
        for(int i=(P+NN)>>1;i;i>>=1){
            querymax[i]=max(querymax[i<<1],querymax[i<<1|1]);
        }
    }
    int Query(int L,int R){
        int res=-inf;
        for(L+=NN-1,R+=NN+1;L^R^1;L>>=1,R>>=1){
            if(!(L&1)) res=max(res,querymax[L^1]);
            if(  R&1 ) res=max(res,querymax[R^1]);
        }
        return res;
    }
};
SegTree t[2];
int ans[N];
bool check(int ca,int cb,int p){
    int kk=ca-cb+V;
    return kk>=0&&kk<=t[kk&1].Query(p,n+1);
}
signed main(){
    n=read();
    int mx=0;
    forup(i,1,n){
        p[i]=read();
        if(p[i]>mx){
            mx=p[i];
            ip[i]=1;
            ++V;
        }
    }
    t[0].Build();t[1].Build();
    t[0].Update(n+1,0);
    fordown(i,n,1){
        int r1=t[1].Query(p[i],n+1),r0=t[0].Query(p[i],n+1);
        if(ip[i]){
            t[1].Update(p[i],r1+2);
            t[0].Update(p[i],r0+2);
        }else{
            t[1].Update(p[i],r0+1);
            t[0].Update(p[i],r1+1);
        }
    }
    if(!check(0,0,0)){
        puts("-1");
        return 0;
    }
    int ma=0,mb=0,ca=0,cb=0;
    forup(i,1,n){
        if(ip[i]) --V;
        t[0].Update(p[i],-inf);
        t[1].Update(p[i],-inf);
        if(check(ca+(p[i]>ma),cb,mb)||check(cb,ca+(p[i]>ma),max(ma,p[i]))){// (1)!
            ans[i]=0;
            if(p[i]>ma){
                ma=p[i];
                ++ca;
            }
        }else{
            ans[i]=1;
            if(p[i]>mb){
                mb=p[i];
                ++cb;
            }
        }
    }
    forup(i,1,n){
        printf("%d",ans[i]);
    }
}
  1. 注意 $P(A'),P(B')$ 都有可能和 $Q(S)$ 相交。

T4

有彼阳出题人把最简单题放 T4。

要维护的东西线段树可以直接维护。

具体来说,对每个结点维护区间内剩余车辆价值总和区间内剩余车辆数量该区间有多少个删除操作无法在区间内部解决

然后合并的时候,考虑右儿子的删除操作对左儿子的影响,然后递归合并,容易发现每次只会进一个儿子所以复杂度是 $O(\log n)$ 的。

总复杂度 $O(n\log^2 n)$,具体如何维护见代码,太板了懒得讲。

参考代码
#include<bits/stdc++.h>
#define mem(a,b) memset(a,b,sizeof(a))
#define forup(i,s,e) for(int i=(s);i<=(e);i++)
#define fordown(i,s,e) for(int i=(s);i>=(e);i--)
using namespace std;
#define gc getchar()
inline int read(){
    int x=0,f=1;char c;
    while(!isdigit(c=gc)) if(c=='-') f=-1;
    while(isdigit(c)){x=(x<<3)+(x<<1)+(c^48);c=gc;}
    return x*f;
}
#undef gc
const int N=2e5+5,inf=0x3f3f3f3f;
int n,m;
struct Node{
    int tp,v;
};
Node a[N];
struct SegTree{
    #define mid ((l+r)>>1)
    #define lson l,mid,id<<1
    #define rson mid+1,r,id<<1|1
    int querysum[N<<2],querycnt[N<<2],queryera[N<<2];
    int work(int ct,int l,int r,int id){
        if(ct==0){
            return querysum[id];
        }
        if(l==r){
            return 0;
        }
        if(ct<=querycnt[id<<1|1]){
            return querysum[id]-querysum[id<<1|1]+work(ct,rson);//(1)!
        }else{
            return work(ct-querycnt[id<<1|1]+queryera[id<<1|1],lson);
        }
    }
    void PushUp(int id,int l,int r){
        if(queryera[id<<1|1]==0){
            querysum[id]=querysum[id<<1]+querysum[id<<1|1];
            querycnt[id]=querycnt[id<<1]+querycnt[id<<1|1];
            queryera[id]=queryera[id<<1];
        }else if(queryera[id<<1|1]<=querycnt[id<<1]){
            querysum[id]=querysum[id<<1|1]+work(queryera[id<<1|1],lson);
            querycnt[id]=querycnt[id<<1|1]+querycnt[id<<1]-queryera[id<<1|1];
            queryera[id]=queryera[id<<1];
        }else{
            querysum[id]=querysum[id<<1|1];
            querycnt[id]=querycnt[id<<1|1];
            queryera[id]=queryera[id<<1]+queryera[id<<1|1]-querycnt[id<<1];
        }
    }
    void Update(int P,Node X,int l=1,int r=n,int id=1){
        if(l==r){
            a[P]=X;
            if(X.tp==1){
                querycnt[id]=querysum[id]=0;
                queryera[id]=X.v;
            }else{
                querycnt[id]=1;
                querysum[id]=X.v;
                queryera[id]=0;
            }
            return;
        }
        if(P<=mid) Update(P,X,lson);
        else       Update(P,X,rson);
        PushUp(id,l,r);
    }
    int Ask(){
        return querysum[1];
    }
}mt;
signed main(){
    n=read();m=read();
    forup(i,1,n){
        int tp=read(),v=read();
        mt.Update(i,Node{tp,v});
    }
    forup(Case,1,m){
        int x=read(),tp=read(),v=read();
        mt.Update(x,Node{tp,v});
        printf("%d\n",mt.Ask());
    }
}
  1. 注意 querysum[id]-querysum[id<<1|1] 才能得到左儿子对当前结点实际的贡献。