The 2024 CCPC National Invitational Contest (Northeast), The 18th Northeast Collegiate Programming Contest 2024 CCPC 东北赛

当时还打过这场比赛,和 yhp cpy。当时啥也不会,只做了三题遗憾打铁,哎 现在 VP 发现当时不会的题好简单

J - Breakfast

纯签

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    double n,m;
    cin>>n>>m;

    double ans=n*0.6+m*1;
    printf("%.2lf",ans);   
}

signed main(){
    // ios::sync_with_stdio(0);
    // cin.tie(0);

    int ct=1;
    // cin>>ct;

    while(ct--){
        solve();
    }
}

D - nIM gAME

手玩一下样例就会发现,全输

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n;
    cin>>n;
    cout<<"lose\n";
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;

    while(ct--){
        solve();
    }
}

A - Paper Watering

先变大再变小一定是不优的,所以只考虑先变小后变大的情况。

如果每次开根号时,发生了下取整,则一定不可能再变成之前的数。对于当前这个数及其多次平方变成的数,全都是新的

否则没有发生下取整,则其平方变成的数一定被之前的数覆盖。只有当前数本身是一个新数

注意特判变成 1 或一开始就是 1 的情况

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n,k;
    cin>>n>>k;
    int ans=k+1;

    int cnt=0;

    if(n==1){
        cout<<1;
        return;
    }

    while(n>1 && k--){
        int ne=sqrt(n);
        ans++;
        
        if(ne*ne!=n && ne!=1){
            ans+=k;
        }
        n=ne;
    }

    cout<<ans;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    // cin>>ct;

    while(ct--){
        solve();
    }
}

E - Checksum

因为 \(k\) 最大是 \(20\),所以直接枚举,后面的二进制表示有几个 \(1\) 就可以了

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

int check(int val){
    for(int i=60;i>=0;i--){
        if((val>>i) & 1) return i+1;
    }
    return 0;
}

void solve(){
    int n,k;
    cin>>n>>k;

    int ans=-1;

    string s;
    cin>>s;

    int cnt=0;
    for(auto ch:s){
        cnt+=(ch=='1');
    }

    for(int i=0;i<=k;i++){
        int now=cnt+i;
        if(__builtin_popcount(now)==i && check(now)<=k){
            ans=now;
            break;
        }
    }

    if(ans==-1){
        cout<<"None\n";
        return;
    }

    vector<int> stk;
    while(ans){
        if(ans&1) stk.push_back(1);
        else stk.push_back(0);
        ans>>=1;
    }

    int nd=k-stk.size();
    while(nd--){
        cout<<0;
    }

    while(stk.size()){
        cout<<stk.back();
        stk.pop_back();
    }

    cout<<endl;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    cin>>ct;

    while(ct--){
        solve();
    }
}

L - Bracket Generation

首先,操作一 () 之间的相互顺序是不能动的

其次,一个 () 后面跟的操作二,是不能比这个 () 早出现的,但可以晚出现

可以从左往右扫,遇到 () 就记为 1,遇到 )) 这样的,就是操作 2,记为 2

得到序列 11211212

根据上面的分析,1 不能乱动,但是 2 可以往后挪。找这种方案数量

等价于对于每个 2 ,对答案累乘 n-i+1

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    string s;
    cin>>s;

    vector<int> a(1);

    for(int i=1;i<s.size();i++){
        if(s[i]==')'){
            if(s[i-1]=='(') a.push_back(1);
            else a.push_back(2);
        }
    }

    int ans=1;
    int n=a.size()-1;

    for(int i=1;i<=n;i++){
        if(a[i]==2) ans*=(n-i+1);
        ans%=mod;
    }

    cout<<ans;
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    // cin>>ct;

    while(ct--){
        solve();
    }
}

F - Factor

等价于,\(p*k*k*...*k~mod~q=0\)

所以提取 \(p\)\(k\) 的质因数并计数,注意 \(k\) 可以有无限个,所以其因数个数也是无限,实际取值 60 就行

然后对这些因数 DFS 可以凑出多少合法的数即可

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int p,x,k;
    cin>>p>>x>>k;

    map<int,int> mp;

    int tmp=p;
    for(int i=2;i*i<=tmp;i++){
        if(tmp%i!=0) continue;

        while(tmp%i==0){
            mp[i]++;
            tmp/=i;
        }
    }
    if(tmp>1) mp[tmp]=1;

    tmp=k;
    for(int i=2;i*i<=tmp;i++){
        if(tmp%i!=0) continue;
        while(tmp%i==0){
            tmp/=i;
        }
        mp[i]=60;
    }
    if(tmp>1) mp[tmp]=60;

    vector<pii> t;
    for(auto [x,y]:mp){
        t.push_back({x,y});
    }
    int ans=0;

    auto dfs=[&](auto dfs,int pos,int sum)-> void { 
        if(pos==t.size()){
            ans++;
            return;
        }

        int val=t[pos].first;
        int cnt=t[pos].second;


        for(int i=0;i<=cnt;i++){
            dfs(dfs,pos+1,sum);
            if((__int128_t)sum*val>x) break;
            sum*=val;
        }
    };

    dfs(dfs,0,1);

    cout<<ans<<endl;

}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    // cin>>ct;

    while(ct--){
        solve();
    }
}

I - Password

“所有位置都是好的”意味着整个序列被若干个长度为 \(k\) 的排列完全覆盖。为了保证相邻两部分没有断层,相邻两个排列起点的距离不能超过 \(k\)。同时,序列的最后 \(k\) 个数一定是一个完整的排列。

我们可以用动态规划来解决,核心在于如何避免合法序列被重复计数。

  1. 基础构件:连通排列(不可分解排列)

设向后扩展序列时,我们想要新增 \(j\) 个数字(\(1 \le j \le k\)),使得它与前面的后缀恰好第一次拼凑成一个新的排列。
为了保证它是“第一次”拼凑成功,这新增的 \(j\) 个数字所构成的结构,不能在内部提前形成完整的排列。这种前缀中不包含任何子排列的序列被称为不可分解排列。

\(g_i\) 表示长度为 \(i\) 的不可分解排列的方案数。
我们通过容斥原理计算 \(g_i\)
从全排列总数 \(i!\) 中,减去那些“不合法”(提前出现子排列)的方案。我们枚举第一次出现完整排列的前缀长度 \(i-j\)(即前 \(i-j\) 个数构成了一个不可分解排列,方案数为 \(g_{i-j}\)),剩下的 \(j\) 个数随意排列(方案数为 \(j!\))。

\[g_i = i! - \sum_{j=1}^{i-1} g_{i-j} \times j! \]

  1. 状态转移:拼接合法序列

定义 \(f_i\) 表示长度为 \(i\),且恰好以一个完整排列结尾的合法序列方案数。

我们枚举距离上一个完整排列的步数 \(j\)
从长度为 \(i-j\) 的合法序列(以排列结尾,方案数为 \(f_{i-j}\))转移过来,向后添加 \(j\) 个数字。为了防止这 \(j\) 个数字在中间某个位置提前凑成排列导致重复统计,这 \(j\) 步的方案数恰好就是我们上面求的 \(g_j\)

因为每次最多跳 \(k\) 步,且跳完后前面的序列长度至少得是 \(k\)(必须有一个基础排列),所以步数 \(j \le \min(k, i-k)\)

\[f_i = \sum_{j=1}^{\min(k, i-k)} f_{i-j} \times g_j \]

初值:\(f_k = k!\)。最终答案即为 \(f_n\)

复杂度分析

预处理 \(g_i\):需要两层循环,复杂度 \(O(k^2)\)

计算 \(f_i\):外层循环 \(n\),内层循环最多 \(k\),复杂度 \(O(nk)\)

总时间复杂度:\(O(nk + k^2)\),在 \(n \le 10^5, k \le 1000\) 的数据范围内可以轻松通过。

已知当前状态的前 \(k\) 个数字已经是一个合法的排列(设为 \(P_{old}\)),我们要向后追加 \(j\) 个数字,使得这 \(j\) 个数字加上 \(P_{old}\) 的后 \(k-j\) 个数字,恰好能拼成一个新的合法排列(设为 \(P_{new}\))。

集合的必然性:

既然 \(P_{new}\) 借用了 \(P_{old}\) 的后 \(k-j\) 个数字,那么为了凑齐 \(1 \sim k\),我们新加进来的这 \(j\) 个数字,在集合层面上,必须完全等同于 \(P_{old}\) 的前 \(j\) 个数字。

提前截断的触发条件:

假设新加的 \(j\) 个数字中,取前 \(m\) 个数字(\(m < j\)),它和之前的数字提前构成了排列。这意味着什么?

这意味着这 \(m\) 个数字,恰好弥补了 \(P_{old}\) 倒数 \(k-m\) 个数字所缺失的部分——也就是这 \(m\) 个数字刚好等同于 \(P_{old}\) 的前 \(m\) 个数字。

同构映射(The Leap):

因为新加的 \(j\) 个数一定是 \(P_{old}\)\(j\) 个数的某种打乱重排。

如果我们把 \(P_{old}\) 的前 \(j\) 个数,按照它们在原序列中的位置,重新离散化映射为 \(1, 2, 3, \dots, j\)

那么“新加的序列中,长度为 \(m\) 的前缀刚好是 \(P_{old}\) 的前 \(m\) 个数”等价于 “长度为 \(m\) 的前缀刚好构成了 \(1 \sim m\) 的排列”。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
using pii=pair<int,int>;
using ll = long long;
using ull = unsigned long long;
const ll inf = 1e18;
const int mod = 998244353;

void solve(){
    int n,k;
    cin>>n>>k;

    if(k>n){
        cout<<0;
        return;
    }

    vector<int> g(n+1,1),f(n+1),fact(n+1,1);
    for(int i=2;i<=n;i++){
        fact[i]=fact[i-1]*i%mod;
    }

    for(int i=2;i<=k;i++){
        g[i]=fact[i];
        for(int j=1;j<i;j++){
            g[i]-=g[j]*fact[i-j];
            g[i]%=mod;
        }
        g[i]=(g[i]%mod+mod)%mod;
    }

    f[k]=fact[k];

    for(int i=k+1;i<=n;i++){
        for(int j=1;j<=k;j++){
            if(i-j<k) break;
            f[i]=(f[i]+f[i-j]*g[j])%mod;
        }
    }

    cout<<f[n];
    
}

signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);

    int ct=1;
    // cin>>ct;

    while(ct--){
        solve();
    }
}
posted @ 2026-04-05 21:30  LYET  阅读(6)  评论(0)    收藏  举报