CSP2019复赛游记

目录

前言:想直接进入正题请跳转到Day 1。

Day -1

明天我就去南京了,感到非常的紧张,毕竟CSP和NOIP没有关系,没有真题可以刷(逃

注:对于内心中难度的比较,是对于NOIP提高组的题目比较的。

Day 0

坐了一趟一个小时却感觉像是一天的火车,终于到了南京。到了南京就去南航去报到,报到完了就去三食堂的二楼吃饭,比学校的饭菜要好吃(SAN值++)。晚上和同学家长吃饭,互相鼓(mo)励(bai)一番以排解压力。回来之后写写线段树(对的,我喜欢写线段树),只需要一点点时间就可以让自己变得开心起来(大雾)

总的来说,在没有被Day1血虐之前,我今天还是比较开心的。

Day 1

考前

进入考场没有什么大问题。试机前半段时间脑抽,bits之类的板子和提醒自己的东西都好久才想起来打,后半段花了大半段时间在搞懂怎么提交(话说我好像曾经来打过一次比赛来着怎么忘了),导致自己没有听清试题电子档的位置,心态瞬间跌入谷底。考前的提醒是放的NOIP2018的,很好笑,考场的人都笑了,稍稍缓解了气氛,如果没有的话心态肯定会受到影响(然后翻车)。所以说,考前一定要有一个开心的心情,心态不能乱。

考中

开局先开T1,感觉难度还没有什么问题,花了40分钟仔仔细细地做了,后来又花了30分钟检查,才意识到即使开了ull也需要特判\(n=64\)的情况,并且把它加入了程序。

此时考试过去了1小时10分钟,还剩2小时20分钟,心态尚且正常,并未受到影响,T1 100分。

打开T2,T2很难,我想了大约30分钟,想出了对于每一个结点都要维护从根节点到它的括号串中以它作为结束的子串的合法括号串个数,然后就不会做了。心想我先把T3的暴力分骗到了再来想T2的正解,于是我就开了T3,略微扫了一眼,幸好我比较菜(真的我没有假),一看就不会做,然后花了30分钟左右写好了10分的暴力。

此时考试过去了2小时10分钟,还剩1小时20分钟,心态已经受到影响,T1 100分,T3 10分。

接下来的时间我都在做T2,由于题目难度激增,知道最后我都没有把T2的正解写出来,只能使用了一个简单的优化策略,就是找一段最小的以它为结束的合法括号子串,之后把它开头的父亲结点的答案加1作为它的答案。这个优化即可骗到80分,虽然没有写出正解是个失误,但是由于暴力写得够快常数够小,并没有造成太大影响。

考试结束,T1 100分,T2 当时估计70分,T3 10分

考后

出了考场,我先和同班同学交流了一下,发现他的预估分数是大约200分左右,当时心态就崩了,虽然现在发现他T1不仅没有特判还写错了被卡成70分,T3爆零,但是对于当时的我来说,还是一个很大的冲击,感觉自己铁定与1=无缘了。

下午,令人惊讶的是,JS的程序很快就发了出来,同时信奥题库上也可以自测了,自测出来是190分(惊奇的和最终分数一样),比同学自测的还略高一点,重拾信心。

Day1结束后,我个人当时的心情应该还是OK的,没有彻底灰心,不过经历了一些大波动,脑海中还是会时不时的掠过诸如即将打铁之类的想法,对于Day2还是有一定影响吧。。。(可能)

做法&程序(考场做法,不能AC)

格雷码

题意

构造题,给出\(n\)和\(k\),要求构造出\(n\)位格雷码中的第\(k\)个。构造方法如下:(抄的题目啦)

  • \(1\)位格雷码由两个\(1\)位二进制串组成,顺序为:\(0\),\(1\)。

  • \(n+1\)位格雷码的前\(2^n\)个二进制串,可以由依此算法生成的\(n\)位格雷码(总共\(2^n\)个\(n\)位二进制串)按顺序排列,再在每个串前加一个前缀\(0\)构成。

  • \(n+1\)位格雷码的前\(2^n\)个二进制串,可以由依此算法生成的\(n\)位格雷码(总共\(2^n\)个\(n\)位二进制串)按逆序排列,再在每个串前加一个前缀\(1\)构成。

做法

直接模拟即可,对于正在求的\(n\)位第\(k\)个字符串,叫做\(f(n,k)\),按照题目中的方式判断:(从一开始标号)

\[ f(n,k)='0'+f(n-1,k)(2k\leq n)\\ f(n,k)='1'+f(n-1,2^n-k)(2k\gt n) \]

程序

记得特判一下\(n=64\)时的情况,此时会爆ull,但是由于要减一,所以可以赋值为\(-1\)来解决。

#include<bits/stdc++.h>
using namespace std;

typedef unsigned long long ull;

string ans;

void dfs(ull n,ull k){
    if(n==0)return;
    if(((ull(1))<<(n-1))>k){
        ans+='0';
        dfs(n-1,k);
    }else{
        ans+='1';
        if(n==64){
            ull tmp=-1;
            tmp-=k;
            dfs(n-1,tmp);
        }else{
            dfs(n-1,((ull(1))<<n)-k-1);
        }
    }
}

ull n,k;

int main(){

    freopen("code.in","r",stdin);
    freopen("code.out","w",stdout);

    cin>>n>>k;
    dfs(n,k);
    cout<<ans<<endl;

    return 0;
    
}

括号树

题意

给你一棵\(1\)为根的树,每个节点上面有一个括号,可能是(或者)。 定义\(s_i\)为:将根结点到\(i\)号结点的简单路径上的括号,按结点经过顺序依次排列组成的字符串。 要求出每一个结点的\(s\)中合法括号子串的个数,合法括号串定义见题目。

做法(理论50分,实际80分)

定义\(pre_i\)为以\(i\)作为结尾的\(s_i\)的合法括号子串的个数。

找到\(s_i\)后缀中最短的合法括号串,定义它的起始为\(l\),结束为\(i\)。然后\(k_i\)就是\(k_{f(l)}+1\)(此处\(f(l)\)指的是\(l\)的父亲)。意义为后缀的最短合法串和之前的合法串连接后的也一定时合法的,也只有它们(和后缀最短合法串本身)是合法的。这个优化比朴素的查找所有的子串在随机数据下要快得多,甚至会因为没有开long longWA而不是TLE所以一定要记得开long long,即使它只是一个暴力。

程序

#include<bits/stdc++.h>
using namespace std;

struct NODE{
    NODE *nxt;
    int val;
    NODE(){
        nxt=NULL;
        val=0;
    }
};

struct LIST{
    NODE *head,*tail;
    LIST(){
        head=new NODE;
        tail=head;
    }
    void push(int val){
        tail->val=val;
        tail->nxt=new NODE;
        tail=tail->nxt;
    }
};

//后期注释:自己实现链表以减小常数

typedef long long ll;

int n;
bool br[500005];
int f[500005];
LIST g[500005];
int ansk[500005];

//a grave for some code
//后期注释:这个是我没想出来的正解的纪念,代表我写过

void brute(int x){
    //后期注释:计算pre
    int brc=0,cur=x,cnt=0;
    while(brc>=0&&cur){
        brc+=(br[cur]?-1:1);
        if(brc==0){
            //后期注释:优化
            ansk[x]=ansk[f[cur]]+1;
            break;
        }
        cur=f[cur];
    }
    for(NODE *it=g[x].head;it!=g[x].tail;it=it->nxt){
        brute(it->val);
    }
}

void calc(int x){
    //后期注释:简单的把做法中的pre计算为k
    ansk[x]+=ansk[f[x]];
    for(NODE *it=g[x].head;it!=g[x].tail;it=it->nxt){
        calc(it->val);
    }
}

int main(){

    freopen("brackets.in","r",stdin);
    freopen("brackets.out","w",stdout);

    scanf("%d\n",&n);
    for(int i=1;i<=n;i++){
        char c;
        scanf("%c",&c);//cerr<<c<<' ';
        br[i]=(c=='(');
    }
    f[1]=0;
    for(int i=2;i<=n;i++){
        scanf("%d",&f[i]);//cerr<<f[i]<<' ';
        g[f[i]].push(i);
    }
    bool brt=false;
    for(int i=2;i<=n;i++){
        if(f[i]!=i-1)brt=true;
    }
    brute(1);
    calc(1);
    ll ans=0;
    for(int i=1;i<=n;i++){
        ans^=((ll)i)*ansk[i];
        //cerr<<i<<": "<<ansk[i]<<endl;
    }
    printf("%lld",ans);

    return 0;
    
}

树上的数

题意

仅仅只需要注意一件事,这个是输入输出都是以数字排列的结点编号!

要好好看题目!不然很可能浪费做题时间!

做法(10分)

next_permutation()你值得拥有。它用于计算当前排列的下一个排列。

程序

#include<bits/stdc++.h>
using namespace std;

int T,n;
int eu[15],ev[15];
int a[15];
int o[15];
int v[15];
int r[15];
int tmp[15];

void solve(){
    memcpy(v,o,sizeof(o));
    for(int j=1;j<n;j++){
        swap(v[eu[a[j]]],v[ev[a[j]]]);
    }
    //后期注释:以边的排列交换边
    for(int i=1;i<=n;i++){
        tmp[v[i]]=i;
    }
    //后期注释:改换为以数字排列的结点编号
    for(int i=1;i<=n;i++){
        if(tmp[i]<r[i]){
            memcpy(r,tmp,sizeof(tmp));
            break;
        }
        if(tmp[i]>r[i])break;
    }
    //后期注释:更改最小字典序的排列
}

int main(){

    freopen("tree.in","r",stdin);
    freopen("tree.out","w",stdout);

    cin>>T;
    while(T--){
        cin>>n;
        for(int i=1;i<=n;i++){
            cin>>tmp[i];
            o[tmp[i]]=i;
        }
        //后期注释:读入排列并更改为以结点编号排列的数字
        memset(r,0x3f,sizeof(r));
        for(int i=1;i<n;i++){
            a[i]=i;
            cin>>eu[i]>>ev[i];
        }
        //后期注释:读入边同时初始化边的排列
        do{
            //后期注释:全排列需要交换的边的顺序
            solve();
        }while(next_permutation(a+1,a+n));
        for(int i=1;i<=n;i++)cout<<r[i]<<' ';
        cout<<endl;
    }

    return 0;
    
}

Day 2

考前

由于众所周知的原因,我今天一开始就没有想要做出任何一道题目的正解。换句话说,我今天就是冲着拿暴力分去的。这是一个看似怂实则更怂的方法,但是他很有用的,有的人死磕T1最后还没搞出来。我还准备了一些糖果用于在昏昏沉沉的时候提神,事实证明这是有用的。

考中

这是我失策的一次,我全程T1T2T3换来换去,导致做题时间被分割成了大约半个小时的一段一段的,没有集中精神思考,全部都只有基本暴力分,最后20分钟还在改自己的程序,局部变量没有初始化这种错误都会犯,幸好NOI LINUX的编译器很好,把我的错误避免了,果然还是太紧张了,考试一定不能太紧张。由于时间分隔太多,对于每道题目的时间分配将在做法中写出。

考后

考完了,心态什么样都没有关系了。滚回去刷题。

做法&程序(考场做法,不能AC)

Emiya 家今天的饭

做法(36分)

没有想法,暴力搜索一波走,36分到手了。由于每一种烹饪方法的菜只有一种,所以就把这个当作是深度,每次选出一道就好了。程序好写方便检查。

程序

#include<bits/stdc++.h>
//后期注释:无用头文件删去
using namespace std;

/*
REMEMBER to use long long and unsigned long long
REMEMBER to check array size
REMEMBER to make brute-force program and use it
*/

typedef long long ll;
const int mod=998244353;

int n,m,k;
int a[105][2005];
int dfscnt[2005];
ll ans;

void dfs(int row,ll cur,int alr){
   //后期注释:row是递归到第几种烹饪方法,cur是目前方法数,alr是选了的菜的数量
   if(alr==k){
       ans=(ans+cur)%mod;
       return;
   }
   //后期注释:判断是否制作了可行的组合,加入到答案里
   if(n-row+1+alr<k){
       return;
   }
   //后期注释:可行性剪枝,然而并不能多骗一点分数。。。
   for(int i=1;i<=m;i++){
       if(dfscnt[i]<(k>>1)&&a[row][i]){
           dfscnt[i]++;
           dfs(row+1,cur*a[row][i]%mod,alr+1);
           dfscnt[i]--;
           //后期注释:由于每一种食材、方法的菜都有很多种,所以直接乘上去它的数量可以多骗点分
       }
   }
   dfs(row+1,cur,alr);
   //后期注释:判断不选菜是否可行
}

int main(){

   ios::sync_with_stdio(false);
   cin.tie(0);
   cout.tie(0);

   freopen("meal.in","r",stdin);
   freopen("meal.out","w",stdout);

   cin>>n>>m;
   for(int i=1;i<=n;i++){
       for(int j=1;j<=m;j++){
           cin>>a[i][j];
       }
   }
   for(k=2;k<=n;k++){
       //后期注释:k是预备选的菜品的总数
       dfs(1,1,0);
   }
   cout<<ans<<endl;

}

划分

做法(64分)

首先,可以证明,确定了某一前缀区间的分配情况后,最后一个子任务尽量小答案才会好。不会证但是感性认识很简单,考场时可以很快想到,我大约想了45分钟想出这个结论。之后就是枚举子任务的大小DP就可以了,只需要写20分钟不到局部变量千万记得要初始化,我差点爆零。

程序

#include<bits/stdc++.h>
//后期注释:无用头文件删去
using namespace std;

//后期注释:同上考前提醒,删去

typedef long long ll;
const ll LLINF=0x3f3f3f3f3f3f3f3f;

struct LIST{
    //后期注释:无用的链表,没用上,完全错误的想法
};

int n,type;
int a[40000005];
ll dpf[5005];
ll dp[5005];
int dpprv[5005];
LIST tsk;

void specgen(){
    //write no code, run nowhere
}

void normread(){
    for(int i=1;i<=n;i++){
        cin>>a[i];
    }
}

void dp5000(){
    ll ans;
    //后期注释:千万记得要初始化,差点爆零
    dp[0]=0;
    //后期注释:dp[i]表示直到i的前缀序列都计算好后最后一个子任务的长度最小是什么
    for(int i=1;i<=n;i++){
        dpf[i]=dpf[i-1]+a[i];
        //后期注释:前缀和计算
        dp[i]=LLINF;
        for(int j=0;j<i;j++){
            if(dp[j]<=dpf[i]-dpf[j]&&dpf[i]-dpf[j]<dp[i]){
                //后期注释:枚举上一个前缀序列,发现更好的情况在转移过去,记录父亲以便计算最后的答案(因为要算平方)
                dp[i]=dpf[i]-dpf[j];
                dpprv[i]=j;
            }
        }
    }
    int cur=n;
    while(cur){
        ans+=(dpf[cur]-dpf[dpprv[cur]])*(dpf[cur]-dpf[dpprv[cur]]);
        cur=dpprv[cur];
        //后期注释:从最后一个结点n开始向着0更新
    }
    cout<<ans<<endl;
}

void greedy(){
    //ohhhhhhhh f***
    //i don't have time to complete it
    //yet another grave here
    //后期注释:当时认为是贪心,后来发现是单调队列,代码没写
}

int main(){

    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    freopen("partition.in","r",stdin);
    freopen("partition.out","w",stdout);

    cin>>n>>type;
    if(type){
        specgen();
    }else{
        normread();
    }
    dp5000();

    return 0;
}

树的重心

想法(40分)

虽然好像做过原题,可是我忘了qaq。那么我就用朴素的方法,枚举树的重心之后计算好了,就是程序写起来很烦的样子。其实还可以多骗一条链的15分的,就是我数组开小了qaq。一定要检查数组啊。

程序

#include<bits/stdc++.h>
//后期注释:无用头文件
using namespace std;

//后期注释:同上提醒

typedef long long ll;

int RP,n,sz[2005],ban,f[2005];
vector<pair<int,int> > g[2005];
ll ans;

int find(int x){
    if(f[x]==x)return x;
    else return f[x]=find(f[x]);
}

void unite(int x,int y){
    x=find(x);
    y=find(y);
    f[x]=y;
}

void dfs(int x,int p){
    sz[x]=1;
    for(int i=0;i<g[x].size();i++){
        int y=g[x][i].first,z=g[x][i].second;
        if(y!=p&&z!=ban){
            dfs(y,x);
            sz[x]+=sz[y];
        }
    }
    //后期注释:计算有根时它的子树大小
}

void mkc(int x,int p){
    bool isc=sz[find(x)]-sz[x]<=(sz[find(x)]>>1);
    for(int i=0;i<g[x].size();i++){
        int y=g[x][i].first,z=g[x][i].second;
        if(y!=p&&z!=ban){
            mkc(y,x);
            isc&=sz[y]<=(sz[find(x)]>>1);
        }
    }
    if(isc)ans+=x;
    //后期注释:查看是否是重心并加入答案
}

int main(){

    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);

    freopen("centroid.in","r",stdin);
    freopen("centroid.out","w",stdout);

    cin>>RP;
    while(RP--){//后期注释:RP--啦(不是)
        cin>>n;
        for(int i=1;i<=n;i++){
            g[i].clear();
        }
        ans=0;
        for(int i=1;i<n;i++){
            int a,b;
            cin>>a>>b;
            g[a].push_back(make_pair(b,i));
            g[b].push_back(make_pair(a,i));
        }
        if(n%10==1){//后期注释:特判链,可惜数组开小了
            for(int i=1;i<n;i++){
                if(((1+i+1)>>1)==((1+i)>>1)){
                    ans+=(1+i)>>1;
                }else{
                    ans+=(1+i);
                }
                if(((n+i+1+1)>>1)==((n+i+1)>>1)){
                    ans+=(n+i+1)>>1;
                }else{
                    ans+=(n+i+1);
                }
            }
            cout<<ans<<endl;
            ans=0;
            continue;
        }
        for(int bn=1;bn<n;bn++){//后期注释:枚举删除的边
            int rt1=-1,rt2=-1;
            ban=bn;
            for(int i=1;i<=n;i++)f[i]=i;
            for(int i=1;i<=n;i++){
                for(int j=0;j<g[i].size();j++){
                    int k=g[i][j].first,l=g[i][j].second;
                    if(l!=ban)unite(i,k);
                }
            }
            for(int i=1;i<=n;i++){
                if(rt1==-1){
                    rt1=find(i);
                }else{
                    if(rt1!=find(i)){
                        rt2=find(i);
                        break;
                    }
                }
            }//后期注释:用并查集制作连通并找到两个根
            dfs(rt1,-1);
            dfs(rt2,-1);
            mkc(rt1,-1);
            mkc(rt2,-1);
        }
        cout<<ans<<endl;
    }

}

Day 3+(后记&提醒)

注意事项:

  • 局部变量初始化
  • 不要因为紧张而把时间分成一小段一小段
  • 不要害怕难,大家都难(除非你像我一样做题时不时脑抽)
  • 数组检查一下,不能只检查直接使用它的子任务,如果有数组复用还要检查复用了它的子任务是否会爆。

好像成绩还行的样子,326分1=,可以试着报各类WC诶(虽然去了也只是充人数)。

上一篇:CSP2019第二轮-划水游记


下一篇:php – 提取位标志的最有效方法