CSP-S2023 全场题解

发布时间 2023-11-03 15:45:21作者: NBest

lock

这题就是个模拟吧,赛时被迷惑了以为是什么不可做题,仔细看只有 \(10^5\) 种状态,那就枚举好了。
我们分别从状态串出发,枚举它能达到的答案,加到 set 取个并集,不过注意给定的状态不能是密码,要减掉。注意不要直接计数器减减,不然如果有相同的算在状态里面的会多减,我考场代码就这么被 \(hack\) 了,好在官方数据没有这种数据。

\(Code\)

set<int>S,s;
int n,a[10][6];
inline int chang(int x){
    return a[x][1]+a[x][2]*10+a[x][3]*100+a[x][4]*1000+a[x][5]*10000;
}
int main(){
    freopen("lock.in","r",stdin);
    freopen("lock.out","w",stdout);
    n=read();
    for(int i=1;i<=n;i++)
       for(int j=1;j<=5;j++)
           a[i][j]=read();
    for(int i=1;i<=n;i++){
        for(int j=1;j<=5;j++){
              for(int o=1;o<=10;o++){
                     a[i][j]=(a[i][j]+1)%10;
                     if(i==1)s.insert(chang(i));
                     else if(S.find(chang(i))!=S.end())s.insert(chang(i));
              }
       }
       for(int j=1;j<=4;j++){
              for(int o=1;o<=10;o++){
                     a[i][j]=(a[i][j]+1)%10;
                     a[i][j+1]=(a[i][j+1]+1)%10;
                     if(i==1)s.insert(chang(i));
                     else if(S.find(chang(i))!=S.end())s.insert(chang(i));
              }
       }
       S=s,s.clear();
    }
    for(int i=1;i<=n;i++)if(S.find(chang(i))!=S.end())S.erase(chang(i));
    cout<<S.size();
    //WA:
    //int ans=S.size();
    //for(int i=1;i<=n;i++)if(S.find(chang(i))!=S.end())ans--;
    //cout<<ans;
    return 0;
}

game

赛时没有想到栈的 \(O(n^2)\) 做法,不然说不定就过了(禁止马后炮,就是你菜),就直接打了个 \(O(n^3)\) 的区间 dp。

\(35pts\ Code\)

const int N=2e6+7;
int n,a[N];
namespace subtask1{
    bool f[802][802];
    bool work(){
        ll ans=0;
        for(int len=2;len<=n;len+=2){
            for(int i=1;i+len-1<=n;i++){
                int j=i+len-1;
                if(a[i]==a[j]&&(len==2||f[i+1][j-1]))f[i][j]=1;
                else{
                    for(int k=i+1;k<j;k+=2){
                        if(f[i][k]&&f[k+1][j]){
                            f[i][j]=1;
                            break;
                        }
                    }
                }
                ans+=f[i][j];
            }
        }
        cout<<ans;
        return 0;
    }
}
int main(){
    string ss;
    cin>>n>>ss;
    for(int i=0;i<n;i++)a[i+1]=ss[i]-'a';
    if(n<=800)return subtask1::work(),0;
    return 0;
}

赛后听说了 \(O(n^2)\) 的栈做法。
关注到我们消的顺序不会对结果造成影响,因此我们随意地能消则消如果消不掉那么就必然不是可消除串。所以我们维护一个没有被消的字符串栈,每加入一个字符就判断是否与栈顶字符相同,相同就删,如果不相同就加入,这样如果中途发现栈空就说明可以消除,计入答案即可。
如果我们直接这样子枚举左端点和右端点消,复杂度是 \(O(n^2)\) 的,考虑优化。
瓶颈在于每次枚举左端点的话有很多内容是与之前的一样的,有很多重复运算,所以不能枚举左端点。然后发现一个如果一个串的后缀是可消的,我们这个要一直枚举左端点直到栈为空,而去掉这个后缀后栈也是一样的。
那么我们是不是只需要找前面与当前串的消除栈相同的串的个数就行了?
可以用哈希或者字典树维护消除串。因为每次只增加或者减少一个字符,所以用字典树不能从头加到尾,而是用一个类似于回跳指针维护;对于哈希就是历史哈希值。时间复杂度 \(O(n)\),用哈希的话时间复杂度平均是 \(O(n\log n)\),但是空间复杂度少了个 \(26\),当然用 unordered_map 可以实现 \(O(n)\),不过复杂度够的话不是很敢用了,之前被卡过。

\(Code\)

//hash
typedef unsigned long long ull;
const ull P=1331,N=2e6+6;
int n,r;
ull a[N],ans;
stack<char>S;
map<ull,int>M;
int main(){
    string ss;
    cin>>n>>ss;
    for(int i=0;i<n;i++,M[a[r]]++)
        if(S.empty()||S.top()!=ss[i]){
            S.push(ss[i]);
            a[++r]=a[r]*P+ss[i]-'a'+1;//must +1!!不然a串与空串无异,卡了挺久
            ans+=M[a[r]];
        }else{
            S.pop(),r--;
            ans+=M[a[r]]+S.empty();
        }
    cout<<ans;
    return 0;
}
//Trie
int n,a[N],tr[26][N],en[N],tot,zero,r;
long long ans;
stack<int>S;
int main(){
    string ss;
    cin>>n>>ss;
    for(int i=0;i<n;i++){
        int w=ss[i]-'a';
        if(S.empty()||S.top()!=w){
            S.push((w));
            if(!tr[w][a[r]])tr[w][a[r]]=++tot;
            a[++r]=tr[w][a[r]];//注意优先级是从右到左
            //等价于a[r+1]=tr[w][a[r]],r++;
            ans+=++en[a[r]]-1;
        }
        else{
            S.pop(),r--;
            if(S.empty())ans+=++zero;
            else ans+=++en[a[r]]-1;
        }
    }
    cout<<ans;
    return 0;
}

本题的类似题目:CF1223F Stack Exterminable Arrays
不过这个因为值域太大不能用字典树,单哈希也能过。

struct

一眼大模拟,考场上没想到正解,没多少时间了就寻思着顺手拿个 \(65\) 分吧,结果因为没有对应的样例,自己的小样例过于水,导致少写两句话也能过自己的样例然后就开开心心地交了。不过 CCF 的慈善数据给我留了 \(30\) 分,非常感谢。

对于 \(65\) 分的 \(ABC\) 部分分,只需要看清楚题目然后想清楚就行了,不需要什么思考。

\(65pts\)

//错因:
//一开始什么东西都没有的时候ERR没有预处理导致很多点RE,结构体加完以后没有把尾部内存加回去,导致地址错误
//再一次体验了什么叫少写两行代码流两行泪。(悲)
int n,tot;//number of struct
struct node{
    string typ;
    ll st;
};
struct Struct{
    string me,typ[102],nam[102];
    int num,len=0,siz=0;
};
map<int,string>S;//address for opt=4
map<string,node>M;//for opt=3
map<string,Struct>Str;
inline int gett(string tpy){
    if(tpy=="byte")return 1;
    if(tpy=="short")return 2;
    if(tpy=="int")return 4;
    if(tpy=="long")return 8;
    return Str[tpy].siz;
}
ll t;
int main(){
    n=read();
    S[0]="ERR";//少写这句
    for(int ww=1;ww<=n;ww++){
        int opt=read();
        if(opt==1){
            Struct p;
            cin>>p.me;
            p.num=read();
            ll now=0;
            for(int i=1;i<=p.num;i++){
                cin>>p.typ[i]>>p.nam[i];
                p.len=max(p.len,gett(p.typ[i]));
            }
            for(int i=1;i<=p.num;i++){
                if(now%gett(p.typ[i])!=0)now=(now/gett(p.typ[i])+1)*gett(p.typ[i]);
                now+=gett(p.typ[i]);
            }
            if(now%p.len!=0)now=(now/p.len+1)*p.len;
            p.siz=now;
            Str[p.me]=p;
            printf("%d %d\n",p.siz,p.len);
        }else if(opt==2){
            string tpy,nam;
            cin>>tpy>>nam;
            if(Str.find(tpy)!=Str.end()){
                Struct p=Str[tpy];
                if(t%p.len!=0)S[t]="ERR",t=(t/p.len+1)*p.len;
                printf("%lld\n",t);
                M[nam]={p.me,t};
                for(int i=1;i<=p.num;i++){
                    if(t%gett(p.typ[i])!=0)S[t]="ERR",t=(t/gett(p.typ[i])+1)*gett(p.typ[i]);
                    S[t]=nam+"."+p.nam[i];
                    M[nam+"."+p.nam[i]]={p.typ[i],t};
                    t+=gett(p.typ[i]);
                }
                if(t%p.len!=0)S[t]="ERR",t=(t/p.len+1)*p.len;//少写这句
                S[t]="ERR";
                continue;
            }
            if(t%gett(tpy)!=0)S[t]="ERR",t=(t/gett(tpy)+1)*gett(tpy);
            printf("%lld\n",t);
            S[t]=nam;
            M[nam]={tpy,t};
            t+=gett(tpy);
            S[t]="ERR";
        }else if(opt==3){
            string w;
            cin>>w;
            printf("%lld\n",M[w].st);
        }else{
            ll addr=read();
            auto it=--S.upper_bound(addr);
            cout<<(it->second)<<endl;
        }
    }
    return 0;
}

思考正解吧,正解肯定不能是完全展开所有变量,因为这个 \(addr\le 10^{18}\)
本题的复杂度瓶颈在结构体嵌套,我们发现除了在操作 \(2\) 定义的变量外,其他的变量都与结构体内部变量一致,所以我们只需要对每个结构体开一个 map,然后每次遇到结构体就递归向内部查找即可,复杂度 \(O(nk\log k)\)

不开 long long 见祖宗
具体细节见代码:

\(Code\)

struct variate{
    string typ,name;
    ll addr;
}ERR;
struct Struct{
    int n,len=0;//元素个数,对齐规则
    ll size=0;//占用大小
    map<ll,variate>atn;//address to name 由相对位置到变量名
    map<string,variate>nta;//name to address(and type)
};
map<string,Struct>str;//类型对应的结构体
map<string,variate>st;//无嵌套的变量查找
map<ll,variate>ad;//无嵌套的地址查找
inline int find(string typ){
    if(typ=="byte")return 1;
    if(typ=="short")return 2;
    if(typ=="int")return 4;
    if(typ=="long")return 8;
    return 0;
}
void define_struct(ll t=0){
    Struct p;//一定要赋初值
    string name;
    cin>>name;
    p.n=read();
    for(int i=1,len;i<=p.n;i++){
        string typ,name;
        cin>>typ>>name;
        len=find(typ);ll siz=len;
        if(!len)len=str[typ].len,siz=str[typ].size;
        p.len=max(p.len,len);
        if(t%len)p.atn[t]=ERR,t=(t/len+1)*len;
        p.atn[t]={typ,name,t};
        p.nta[name]={typ,name,t};
        t+=siz;
    }
    if(t%p.len)p.atn[t]=ERR,t=(t/p.len+1)*p.len;
    p.size=t;
    p.atn[t]=ERR;
    str[name]=p;
    printf("%lld %d\n",p.size,p.len);
}
int n;ll t;
inline void define_element(){
    string typ,name;
    cin>>typ>>name;
    int len;ll siz;//这里siz要ll
    if(str.find(typ)!=str.end())len=str[typ].len,siz=str[typ].size;
    else len=siz=find(typ);
    if(t%len)ad[t]=ERR,t=(t/len+1)*len;
    printf("%lld\n",t);
    st[name]={typ,name,t};
    ad[t]={typ,name,t};
    t+=siz;
    ad[t]=ERR;
}
void search_element(){
    string name,top;
    int lentop;
    cin>>name;
    for(lentop=0;lentop<name.length()&&name[lentop]!='.';lentop++);
    top=name.substr(0,lentop);
    if(lentop==name.length())return printf("%lld\n",st[top].addr),void();
    Struct from=str[st[top].typ];
    ll addr=st[top].addr;
    while(name.length()>lentop){
        name=name.substr(lentop+1);
        for(lentop=0;lentop<name.length()&&name[lentop]!='.';lentop++);
        top=name.substr(0,lentop);
        addr+=from.nta[top].addr;
        if(name.length()>lentop)from=str[from.nta[top].typ];
    }
    printf("%lld\n",addr);
}
void search_address(){
    ll addr=read();
    variate p=(--ad.upper_bound(addr))->second;
    string name=p.name;
    addr-=p.addr;
    while(!find(p.typ)){
        name+=".";
        Struct o=str[p.typ];
        p=(--o.atn.upper_bound(addr))->second;
        name+=p.name;
        addr-=p.addr;
        if(p.name=="ERR")name="ERR";
    }
    cout<<name<<endl;
}
int main(){
    freopen("struct.in","r",stdin);
    freopen("struct.out","w",stdout);
    n=read();
    ad[0]=ERR={"long","ERR",0};
    while(n--){
        int opt=read();
        if(opt==1)define_struct();
        else if(opt==2)define_element();
        else if(opt==3)search_element();
        else search_address();
    }
    return 0;
}

打大模拟的时候一定要注意力集中,不能凭借肌肉记忆,不然打代码一时,调代码一世。认真打的话就可以微调过了。(虽然写其他的题目每一步也需要非常清楚自己在干什么,不能神游)。

tree

考场上第二接近满分的题,虽然也能随便被 hack,但是在 ccf 上也有 90 分的好成绩。
在我被软件问题调试和第一题浪费了 \(1\) 个小时后,又被第二题和第三题的没有思路折磨的不想打时,是它让我重拾信心。
看见他的第一眼:树题唉,大概率树形 dp,之前模拟赛做飞起来了,可能可做?
第二眼:最小答案?保证在 \(10^9\) 内有解?裸的二分答案加树形 dp 啊。
第三眼:关系线性?分类讨论加等差数列公式,懒得管推什么公式了,\(10^5\) 直接再来一个 \(\log n\) 跑二分答案!找出对于当前天数至少需要从什么时候开始种树。再来个树上贪心吧,对每一个点记录最晚什么时候到即可(我当时想的是对儿子排个序然后每个儿子减去对应序号取最小值,但是因为每次服务的不一定是同一棵子树,所以这样不一定合法。正解是取值最小的儿子 \(-1\),然后排序看看能不能满足条件)。

复杂度 \(O(n\log^2 n)\),准能过。

然后打代码就完事了。因为 ccf 样例太水导致我甚至等差数列的公式打错了都能全部通过。所以也预料到了赛后被 hack。

\(Code\)

typedef __int128 INT;
int n;
int cal1(int las,ll b,ll c,INT a){
    INT h2=b+las*c;
    if(h2>=a)return n;
    int l=0,r=n;
    while(l<r){
        int mid=(l+r+1>>1);
        if((INT)(h2+b+c*mid)*(las-mid+1)>=a*2)l=mid;//A.P
        else r=mid-1;
    }
    return l;
}
int cal2(int las,ll b,ll c,INT a){
    ll o=(b-1)/(-c);//f(o)>=1
    int l=0,r=n;
    while(l<r){
        int mid=(l+r+1>>1);
        INT S;
        if(mid>o)S=las-mid+1;
        else if(las>o)S=(INT)((INT)b*2+c*(o+mid))*(o-mid+1)/2+(las-o);
        else S=(INT)((INT)b*2+c*(las+mid))*(las-mid+1)/2;
        if(S>=a)l=mid;
        else r=mid-1;
    }
    return l;
}
ll a[N],b[N],c[N],t[N],f[N],id[N];
vector<int>F[N];
bool dfs(int x,int fa){
    f[x]=t[x];
    for(int i=0;i<F[x].size();i++){
        int y=F[x][i];
        if(y==fa)continue;
        if(!dfs(y,x))return 0;
        f[x]=min(f[x],f[y]-1);
        if(f[x]<1)return 0;
    }
    return 1;
}
inline bool cmp(int x,int y){
    return f[x]<f[y];//排序小技巧
}
bool check(int x){
    for(int i=1;i<=n;i++){
        id[i]=i;
        if(c[i]>=0){
            int w=cal1(x,b[i],c[i],a[i]);
            if(w==0)return 0;
            t[i]=w;
        }else{
            int w=cal2(x,b[i],c[i],a[i]);
            if(w==0)return 0;
            t[i]=w;
        }
    }
    if(!dfs(1,0))return 0;
    sort(id+1,id+1+n,cmp);
    for(int i=1;i<=n;i++)
        if(i>f[id[i]])return 0;
    return 1;
}
int main(){
    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read(),b[i]=read(),c[i]=read();
    }
    for(int i=1;i<n;i++){
        int u=read(),v=read();
        F[u].push_back(v);
        F[v].push_back(u);
    }
    int l=n,r=1e9+2;
    while(l<=r){
        int mid=l+r>>1;
        if(check(mid))r=mid-1;
        else l=mid+1;
    }
    cout<<l;
    return 0;
}

后记

不得不说这次 \(CSP-S\) 真的不难,高估了。希望 \(NOIP\) 也不要太难啊,rp 加满!

finished at 2023.11.3 15:36