1. 程式人生 > 其它 >2021 CCPC 威海 部分題解

2021 CCPC 威海 部分題解

順序按照難易程度(參考過題人數)

-J Circular Billiard Table

  • 題意描述:從圓盤某個邊緣以與豎直方向夾\(\alpha\)角射入一顆小球,小球在圓盤內部沿反射規律運動,求解小球在第一次回到原點之前共發生幾次碰撞
  • 經過分析,我們可知小球在反射過程中圓心角始終保持不變,而我們這個圓心角\(\theta=2\alpha\)顯然,所以我們可以得知小球反射\(n\)次回到原點,一定有\(n\cdot\theta=k\cdot360\),其中\(k\)為某個正整數
  • 所以我們需要求解最小的\(n\)滿足有\(360|n\theta\)
  • 而要求解\(a|n*b\)的最小正整數\(n\),我們可知\(n=\frac{a}{gcd(a,b)}\)
  • 將數值代入上述公式後即可求解
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll T,a,b;
int prime[1000005];
int cnt=0;
vector<int> ans;
int div(int x){
    for(int i=1;i<=cnt;i++){
        if(x==1)break;
        if(x%prime[i]){
            x/=prime[i];
            ans.push_back(prime[i]);
            i-=1;
        }
    }
}   


int main(){
    //cout<<__gcd(50,360)<<endl;
    cin>>T;
    while(T--){
        scanf("%lld %lld",&a,&b);
        ll x=b*180;
        // int i;
        // for(i=1;;i++){
        //     if(i*a % x==0)break;
        // }     
        // cout<<i-1<<endl;
        cout<<(x/__gcd(a,x))-1<<endl;
        
    }
    return 0;
}

-D Period

  • 題意描述:規定一個整數\(T\),如果其是字串\(s\)的一個\(period\)當且僅當對於任意\(i\in (T,|s|]\),均有\(s[i]=s[i-T]\)並且有\(1\le T\le |s|\),我們給定一個只包含小寫字母的字串,並且給定\(q\)個詢問,每個詢問將字串的某個位置修改為#,且修改之間相互獨立,求當前修改情況下字串的\(period\)
  • 本題中所有的詢問都只有字串中的一個位置改變,針對題目內容詢問處理相對容易,我們先對原字串進行預處理,通過預處理結果與修改位置我們可以得到每次詢問的答案。
  • 所以本題的關鍵在於如何進行預處理,題目中的\(period\)
    自然讓我們聯想到\(kmp\)演算法,我們可以先求出原字串的\(next\)陣列,再借助\(next\)陣列做進一步的求解
  • 得到\(next\)陣列之後,因為原字串中第\(i\)位應該對應\(next\)陣列中的第\(i-1\)位,所以我們初始化上界的值應該為\(j=len\),隨後我們藉助\(next\)數不斷向前回溯,做\(j=nex[j]\),對於\(nex[j]!=0\),我們可以說明該字串存在一個長度為\(nex\)\(period\)
  • 我們對於出現過的回溯過程中出現過的\(nex[j]\)做統計,最後計算字首和陣列\(a[i]\)表示字串有多少個長度在\(i\)以內的\(period\)
  • 對於當前修改位置\(x\)的字母為#,顯然長度大於等於\(x\)\(period\)都顯然不再能夠稱為\(period\),但因為\(period\)的長度是同時從隊首元素和隊尾元素考慮的,所以我們針對\(x\)限制長度時也需要從隊首隊尾分別考慮,我們做\(x=min(x-1,len-x)\),得到的\(x\)顯然就是上限長度,直接代入陣列\(a\)就是當前詢問的答案
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+5;
char s[maxn];
int q,len;
int nex[maxn],a[maxn]; 
// aabaaba
// 如何對period進行求解

void Getnext(){
    int j=0,k=-1;
    nex[0]=-1;
    while(j<=len-1){
        if(k==-1||s[j]==s[k]){
            j++;
            k++;
            nex[j]=k;
        }else{
            k=nex[k]; //向前回溯
        }
    }

}
void prenext(){
    int j=len;
    while(j>0){
        a[nex[j]]+=1;
        j=nex[j];
    }
    a[0]=0;
    for(int i=1;i<=len-1;i++){
        a[i]+=a[i-1];
    }
}

int main(){
    scanf("%s",s);
    len=strlen(s);
    Getnext();
    prenext();
    scanf("%d",&q);
    // 先對字串進行預處理
    // 通過陣列記錄來處理詢問相關操作
    for(int i=1;i<=len;i++){
        cout<<nex[i]<<' ';
    }
    cout<<endl;
    for(int i=1;i<=len;i++){
        cout<<a[i]<<' ';
    }
    // cout<<nex[3]<<endl;
    // cout<<nex[len-1]<<endl;
    while(q--){
        int x;
        scanf("%d",&x);
        x=min(x-1,len-x);
        printf("%d\n",a[x]);
    }

    return 0;
}

-G Desserts

  • 題意描述:\(n\)件物品,每件物品有\(a[i]\)件,我們需要將這\(n\)件物品全部分給至多\(m\)個人,每個人每種物品至多隻能拿一件,求對於範圍\([1,m]\)內每種人數的分配方案數
  • 樸素思想:易知,只有當分配人數大於\(n\)種物品中最大件數時,才能完全分配完所有產品,因此方案數才不為0。此時對於第\(i\)件產品,我們顯然有\(C_{t}^{a[i]}\)種分配策略,其中\(t\)為當前的人數,滿足\(1\le t\le m\),此時我們只需要將所有產品的分配策略相乘即可得到在當前人數下的分配方案總數,時間複雜度為\(O(mn)\),顯然會發生超時,所以我們接下來需要思考如何優化時間複雜度
  • 優化(本題最難想到的點):我們注意到題目中有條件\(\sum^n_{i=1}a_i\le 10^5\),所以我們可以知道對於\(n\)\(a_i\)我們最多僅能有\(\sqrt{2*10^5}\)\(a_i\)互異\((\frac{n(n-1)}{2}\le10^5)\),我們能夠使用\(unordered\_set\)記錄下所有不同的\(a[i]\)及其出現次數,對於出現多次的\(a[i]\)我們使用快速冪演算法處理即可,最後在不同的人數下下將\(unordered\_set\)中每個\(a_i'\)的結果計算並相乘即可得到最終結果。
  • 參考程式碼
#include<bits/stdc++.h>
using namespace std;
#define mod 998244353 
#define ll long long
const int maxn = 5e4+5;
const int M = 1e5+5;
int n,m;
int a[maxn],cnt[maxn]; // 後者用來記錄a_i相同數字出現的次數 
unordered_set<int> at; // 用來記錄去重之後的a陣列

// 暴力列舉 O(nm) 顯然超時
// 所以本題的關鍵在於如何快速進行計算

ll exp(ll a,ll b){
    ll tmp=1;
    while(b){
        if(b&1)tmp=(a*tmp)%mod;
        a=(a*a)%mod;
        b/=2;
    }
    return tmp%mod;
}


ll fac[M],finv[M];
ll inv(ll x){
    return exp(x,mod-2);
}
int init(){
    fac[0]=1;
    for(int i=1;i<=M-5;i++){
        fac[i]=fac[i-1]*i%mod;
    }
    finv[M-5]=inv(fac[M-5]);
    for(int i=M-6;i>=0;i--){    
        finv[i]=finv[i+1]*(i+1)%mod;
    }
}

ll C(ll n,ll m){ // 快速計算
    if(m>n)return 0;
    return fac[n]*finv[m]%mod*finv[n-m]%mod;
}

int main(){
    init();
    cin>>n>>m;
    int maxx=0;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        maxx=max(maxx,a[i]);
    }
    for(int i=1;i<=n;i++){
        cnt[a[i]]++;
        at.insert(a[i]);
    }
    for(int i=1;i<=m;i++){
        if(i<maxx){ // 無法完全分配
            printf("0\n");
            continue;
        }
        ll ans=1;
        for(auto x:at){
            ans=(ans*exp(C(i,x),cnt[x]))%mod;
        }
        printf("%lld\n",ans); // 輸出量較大,使用printf
    }
    return 0;
}