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;
}

-M 810975

  • 参考题解:链接
  • 在完成这道题以前我们先考虑一道题目

HDU 6397

  • 题意描述:给定\(n,m,k\),要求我们选定\(m\)个范围在\([0,n-1]\)中的数,使得这\(m\)个数的和为\(k\)

  • 我们先在选定数无上限的条件下考察该问题,显然我们可以将\(k\)看作\(k\)\(1\),我们通过\(m-1\)个隔板来将这\(k\)\(1\)划分为\(m\)个数,但由于数字可以取\(0\),而隔板法显然不能够满足数字选\(0\)的需求,此时我们可以做一个等价处理,将选取的\(m\)个数的下限都加\(1\),保障其不能取\(0\),这样我们所有数的和应该为\(k+m\),所以我们使用隔板法可以知道方法数应该为\(C_{k+m-1}^{m-1}\)

  • 接下来我们考虑有选取的\(m\)限制的情况,我们已知无限制情况下的方案总数,所以我们可以利用容斥原理来考虑有限制情况下的方案总数

  • 借助容斥原理,问题转化为求解选择\(m\)个数,其中至少有\(i\)个数大于\(n-1\),且\(m\)个数的和为\(k\)的方案总数。要实现至少有\(i\)个数大于等于\(n\),我们可以将不限定\([0,n-1]\)区间选择出来的\(m\)个数其中的\(i\)个数分别加上\(n\),保障这些数一定满足大于\(n\),而此时我们选择的数据总和应该为\(k-n*i\),以保障增加之后数据之和为\(k\),利用我们考虑无上限情况时的方法,我们可以知道情况数应该为\(C_{m}^i\times C_{k+m-1-n}^{m-1}\)

  • 记有至少\(i\)个数大于\(n-1\)为的情况总数为\(f(i)\),则所求结果应当为\(f(0)-f(1)+f(2)-\cdots+f(t)\),其中\(t=min(n,k/n)\)

  • 参考代码

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int M = 1e6+5;
const int mod = 998244353;
int T,n,m,k;

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

ll fac[M],finv[M];
ll inv(ll x){
    return qsm(x,mod-2);
}

void 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>>T;
    while(T--){
        cin>>n>>m>>k;
        int f=1;
        int ans=0;
        for(int i=0;i<=m;i++){
            int tmp=1;
            if(!f)tmp=mod-1; // 1的
            f^=1;
            if(1ll*i*n>k+m-1)break;
            ans=(1ll*ans+1ll*tmp*C(m,i)%mod*C(k+m-1-i*n,m-1)%mod);
        }
    }

    return 0;
}
  • 接下来让我们回到题目本身

CCPC Weihai -M 810975

  • 题意描述:给定\(n,m,k\),要求构造长度为\(n\)\(01\)串,其中有且仅有\(m\)\(1\),且连续\(1\)段落的最大长度恰为\(k\),求满足条件的\(01\)串总数
  • 本题要求串中连续\(1\)段落的最大长度恰好为\(k\),我们此处可以使用容斥原理,考虑连续\(1\)段落上限为\(k\)的情况和连续\(1\)段落上限为\(k-1\)的情况,并利用前者减去后者即可
  • 所以接下来我们考虑的问题即是求满足长度为\(n\)\(1\)个数为\(m\),且连续\(1\)段落上限为\(k\)\(01\)串数目
  • 本题中的\(0\)我们可以视为隔板,利用\(0\)来将\(1\)划分为不同的连续段落,而因连续\(1\)段落的上限为\(k\),所以我们可以知道任意两个\(0\)之间\(1\)的数量的取值范围应该为\([0,k]\),并且我们可以知道所有连续\(1\)段落的和应该为\(m\)
  • 如此我们可以将问题转化为选取\(n-m+1\)个数,要求这\(n-m+1\)个数的取值范围在\([0,k]\),并且总和为\(n\)
  • 这样本题求解的问题可以直接类比到HDU 6397当中,结合其中的分析过程我们可以得到的代码
  • 当然需要注意上面题目中的取值界限\(n\)要求不能取到而本题中取值界限\(k\)要求可以取到,需要做适当的转化
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int mod = 998244353;
const int M = 1e5+5;
// 有m个1,n-m个0,求最大1连续长度恰好为5的01串个数
// 7个1 2个0 长度限定5
// 

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

ll fac[M],finv[M];
ll inv(ll x){
    return qsm(x,mod-2);
}
void 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;
}

ll cal(ll num,ll sum,ll up){ 
    int f=1;
    ll ans=0;
    for(int i=0;i<=num;i++){
        int tmp=1;
        if(!f)tmp=mod-1;  // 用于做减法运算

        f^=1;
        if(i*up>sum+num-1)break;
        ans=(ans+tmp*C(num,i)%mod*C(sum+num-1-i*up,num-1)%mod)%mod;
        //cout<<ans<<endl;
    }
    return ans;
}



int main(){
    init();
    int n,m,k;
    cin>>n>>m>>k;
    cout<<(cal(n-m+1,m,k+1)-cal(n-m+1,m,k)+mod)%mod<<endl;
    //cout<<cal(n-m+1,m,k)<<endl;
    //cout<<cal(m,n-m+1,k)<<endl;
    return 0;
}
// 靠左、中间、靠右
// 任何连续1段落必须包含在两个0之间 或者 边缘与1个0之间
// 我们可以在零一串的首位加上两个哨兵0,则所有的连续1段落都在0之中
// 原题中存在 n-m个0 ,则可以存在n-m+1个1段落
// 将m个1在这n-m+1个段落中进行分配
// 
posted @ 2022-01-07 21:29  Ghaser  阅读(1917)  评论(2)    收藏  举报