CSP-S 2019 遊記

date: 2019-11-15c++

前言:想直接進入正題請跳轉到Day 1。算法

Day -1

明天我就去南京了,感到很是的緊張,畢竟CSP和NOIP沒有關係,沒有真題能夠刷(逃數組

注:對於心裏中難度的比較,是對於NOIP提升組的題目比較的。優化

Day 0

坐了一趟一個小時卻感受像是一天的火車,終於到了南京。到了南京就去南航去報到,報到完了就去三食堂的二樓吃飯,比學校的飯菜要好吃(SAN值++)。晚上和同窗家長吃飯,互相鼓(mo)勵(bai)一番以排解壓力。回來以後寫寫線段樹(對的,我喜歡寫線段樹),只須要一點點時間就可讓本身變得開心起來(大霧)spa

總的來講,在沒有被Day1血虐以前,我今天仍是比較開心的。code

Day 1

考前

進入考場沒有什麼大問題。試機前半段時間腦抽,bits之類的板子和提醒本身的東西都很久纔想起來打,後半段花了大半段時間在搞懂怎麼提交(話說我好像曾經來打過一次比賽來着怎麼忘了),致使本身沒有聽清試題電子檔的位置,心態瞬間跌入谷底。考前的提醒是放的NOIP2018的,很可笑,考場的人都笑了,稍稍緩解了氣氛,若是沒有的話心態確定會受到影響(而後翻車)。因此說,考前必定要有一個開心的心情,心態不能亂。orm

考中

開局先開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誒(雖然去了也只是充人數)。

相關文章
相關標籤/搜索