第十一届中国大学生程序设计竞赛 郑州站(CCPC 2025 Zhengzhou Site)

Preface

今年的 Regional 全部结束后就有点半摆烂了,旷了两周没训练,把精力都放在论文写作和出去旅游上了

这场 VP 之前就知道题目比较毒瘤,因此打的时候也不着急慢慢开题,最后也是堪堪六题金尾


B. Cutting Chocolate

首先 \(n\) 必须是 \((p+1)(q+1)(r+1)\) 的倍数

然后对于每一个维度,将所有点按顺序排列,该维度上的刀只能在对应均分的两个点之间砍,因此很容易计算方案数

但还要注意检验一种切法是否有解,具体可以拿一个桶统计下分到每个部分里的数量是否都相同

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<array>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,mod=1e9+7;
array <int,4> a[N]; array <int,3> d[N];
int L,W,H,p,q,r,n; map <array <int,3>,int> bkt;
int main()
{
    scanf("%d%d%d%d%d%d%d",&L,&W,&H,&p,&q,&r,&n);
    for (RI i=1;i<=n;++i)
    {
        for (RI j=0;j<3;++j)
        scanf("%d",&a[i][j]);
        a[i][3]=i;
    }
    int tn=n;
    if (tn%(p+1)!=0) return puts("0"),0; tn/=(p+1);
    if (tn%(q+1)!=0) return puts("0"),0; tn/=(q+1);
    if (tn%(r+1)!=0) return puts("0"),0; tn/=(r+1);
    // puts("has solution");
    int ans=1;
    for (RI j=0;j<3;++j)
    {
        auto cmp=[&](const array <int,4>& A,const array <int,4>& B)
        {
            return A[j]<B[j];
        };
        sort(a+1,a+n+1,cmp);
        int num=n;
        if (j==0) num/=(p+1);
        if (j==1) num/=(q+1);
        if (j==2) num/=(r+1);
        for (RI i=1;i<=n;++i)
        {
            d[a[i][3]][j]=(i-1)/num;
            // printf("%d\n",a[i+1][j]-a[i][j]);
            if (i%num==0&&i!=n) ans=1LL*ans*(a[i+1][j]-a[i][j])%mod;
        }
    }
    for (RI i=1;i<=n;++i) ++bkt[d[i]];
    int flag=1,val=bkt.begin()->second;
    for (auto [_,c]:bkt)
    if (c!=val) { flag=0; break; }
    if (!flag) puts("0"); else printf("%d\n",ans);
    return 0;
}

D. Diameter of a Tree

思路很好想但实现起来需要一些细节的题,感觉这种题每次 VP 都能写出来但赛场上压力一大就不知道会漏什么 Case(比如今年 CCPC 重庆的 F)

首先我们只保留所有可能在直径上的点,并且根据经典结论,所有直径有一个公共中点(长度为偶数时)或一条公共中边(长度为奇数时)

为了避免分讨,可以通过建一个新点连接公共中边的两个端点,把两种情况合在一起

考虑此时答案会如何构成,设此时图中叶子节点数为 \(k\),显然 \(1\sim k\) 要分给每个叶子

因此字典序最大的路径刚开始一段一定是从 \(k\) 对应的叶子开始,\(k+1,k+2,\ldots\) 一直到直径的中点

而后半部分手玩一下会发现和经过的每个点的度数有关,只要最小化经过的点的度数序列的字典序就能构造出最优解

从直径的公共中点开始 BFS,每次只保留同一层度数最小的那些点即可找出最优的路径,注意最后一步走叶子时需要特判

具体实现看代码,想清楚了还是很好写的

#include<cstdio>
#include<iostream>
#include<vector>
#include<queue>
#include<algorithm>
#include<assert.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int t,n,D,mx[N],smx[N],out[N],valid[N],pre[N]; vector <int> v[N],nv[N];
inline void DFS1(CI now=1,CI fa=0)
{
    for (auto to:v[now])
    {
        if (to==fa) continue;
        DFS1(to,now);
        if (mx[to]+1>mx[now]) smx[now]=mx[now],mx[now]=mx[to]+1;
        else if (mx[to]+1>smx[now]) smx[now]=mx[to]+1;
    }
}
inline void DFS2(CI now=1,CI fa=0)
{
    for (auto to:v[now])
    {
        if (to==fa) continue;
        out[to]=max(out[to],out[now]+1);
        if (mx[now]==mx[to]+1) out[to]=max(out[to],smx[now]+1);
        else out[to]=max(out[to],mx[now]+1);
        DFS2(to,now);
    }
    D=max(D,mx[now]+max(smx[now],out[now]));
}
inline int BFS1(CI st)
{
    static int len[N];
    for (RI i=1;i<=n;++i) pre[i]=-1,len[i]=0;
    queue <int> q; q.push(st); pre[st]=0;
    while (!q.empty())
    {
        int now=q.front(); q.pop();
        for (auto to:v[now])
        if (pre[to]==-1)
        {
            pre[to]=now; q.push(to);
            len[to]=len[now]+1;
        }
    }
    int pos=1;
    for (RI i=2;i<=n;++i)
    if (len[i]>len[pos]) pos=i;
    return pos;
}
inline int BFS2(CI st)
{
    static int len[N],layer[N];
    for (RI i=1;i<=n+1;++i) pre[i]=-1,len[i]=0,layer[i]=n;
    queue <int> q; q.push(st); pre[st]=0; layer[0]=(int)nv[st].size();
    while (!q.empty())
    {
        int now=q.front(); q.pop();
        if ((int)nv[now].size()!=layer[len[now]]) continue;
        for (auto to:nv[now])
        if (pre[to]==-1)
        {
            pre[to]=now; q.push(to);
            len[to]=len[now]+1;
            layer[len[to]]=min(layer[len[to]],(int)nv[to].size());
        }
    }
    int mxlen=0;
    for (RI i=1;i<=n+1;++i)
    mxlen=max(mxlen,len[i]);
    for (RI i=1;i<=n+1;++i)
    if (len[i]==mxlen-1&&(int)nv[i].size()==layer[len[i]]) return i;
    assert(0); return -1;
}
int main()
{
    for (scanf("%d",&t);t;--t)
    {
        scanf("%d",&n);
        for (RI i=1;i<=n+1;++i)
        {
            v[i].clear(); nv[i].clear();
            mx[i]=smx[i]=out[i]=valid[i]=0;
        }
        for (RI i=1;i<n;++i)
        {
            int x,y; scanf("%d%d",&x,&y);
            v[x].push_back(y);
            v[y].push_back(x);
        }
        D=0; DFS1(); DFS2();
        for (RI i=1;i<=n;++i)
        if (mx[i]+max(smx[i],out[i])==D) valid[i]=1;
        int endpnt;
        for (RI i=1;i<=n;++i)
        if (valid[i]&&(int)v[i].size()==1) endpnt=i;
        endpnt=BFS1(endpnt);
        vector <int> path;
        while (endpnt!=0)
        {
            path.push_back(endpnt);
            endpnt=pre[endpnt];
        }
        int rt;
        if (D%2==0)
        {
            rt=path[(int)path.size()/2];
            // printf("rt = %d\n",rt);
            for (RI x=1;x<=n;++x)
            for (auto y:v[x])
            if (valid[x]&&valid[y])
            {
                nv[x].push_back(y);
                // printf("(%d, %d)\n",x,y);
            }
        } else
        {
            int a=path[(int)path.size()/2-1];
            int b=path[(int)path.size()/2];
            rt=n+1;
            // printf("rt = %d\n",rt);
            nv[rt].push_back(a); nv[a].push_back(rt);
            nv[rt].push_back(b); nv[b].push_back(rt);
            // printf("(%d, %d)\n",rt,a);
            // printf("(%d, %d)\n",rt,b);
            for (RI x=1;x<=n;++x)
            for (auto y:v[x])
            if (valid[x]&&valid[y])
            {
                if ((x==a&&y==b)||(x==b&&y==a)) continue;
                nv[x].push_back(y);
                // printf("(%d, %d)\n",x,y);
            }
        }
        int k=0;
        for (RI i=1;i<=n;++i)
        if ((int)nv[i].size()==1) ++k;
        vector <int> ans;
        for (RI i=0;i<=D/2;++i)
        ans.push_back(k+i);
        int val=k+D/2;
        endpnt=BFS2(rt); path.clear();
        while (endpnt!=0)
        {
            path.push_back(endpnt);
            endpnt=pre[endpnt];
        }
        reverse(path.begin(),path.end());
        for (auto x:path)
        ans.push_back(val+=(int)nv[x].size()-1);
        ans.pop_back();
        ans.push_back((int)nv[path.back()].size()-1);
        for (RI i=0;i<(int)ans.size();++i)
        printf("%d%c",ans[i]," \n"[i==(int)ans.size()-1]);
    }
    return 0;
}

G. Plus Xor

队友开场写的,我题目都没看

#include <bits/stdc++.h>

using llsi = long long;

std::unordered_map<llsi, bool> dp;
llsi a, b, c, B;

bool test(llsi a) {
    return c >= a && (c - a) % b == 0;
}

bool dfs(llsi a, llsi step) {
    if(step > B) return false;
    if(test(a) || test(a ^ b)) return true;
    if(auto it = dp.find(a); it != dp.end()) return it->second;
    return dp[a] = dfs(a + b, step + 1) || dfs((a ^ b) + b, step + 1);
}

bool work() {
    std::cin >> a >> b >> c;
    dp.clear();
    B = 1;
    while(B < b) B *= 2;
    return dfs(a, 0);
}

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) std::cout << (work() ? "YES\n" : "NO\n");
    return 0;
}

I. Dumb Problem II

发现一段时间不训练后思维能力会得到显著的上升,比如 Counting 都可以很快 solo 出来

考虑对于一个固定的 \(g(p)\) 集合,要计算有多少种排列能生成它是 trivial 的

例如对于 \(n=5,g(p)=\{2,5\}\),则有 \(1\times 2\times 4\) 中方案,其中 \(1,2,4\) 分别对应在固定的 \([2,5]\) 中依次插入 \(4,3,1\) 的方案数

因此很容易想到从大到小加数 DP,令 \(f_{i,j}\) 表示已经加入了 \([i,n]\) 中的数,其中 \(g(p)\) 集合里最小的数是 \(j\) 时,对应的原排列的数量

加入 \(i-1\) 时,如果 \(i-1\)\(g(p)\) 中,则转移到 \(f_{i-1,i-1}\),只有一种放法(放在开头);否则转移到 \(f_{i-1,j}\),有 \(n-i+1\) 种放法

现在考虑有了 DP 信息后怎么计算答案,对于某个固定的 \(g(p)\) 集合,它对应 \(c\) 种不同的原排列,那么生成出它的概率就是 \(p=\frac{c}{n!}\)

由于抽取 \(k\) 次的独立性,它对答案的贡献就是 \(1-(1-p)^k\),最后整体求和即可

但我们前面 DP 求出的信息本质上是 \(\sum c\),即等价于 \(\sum p\),不能直接用于计算 \(\sum 1-(1-p)^k\)

不过通过用二项式定理将 \((1-p)^k\) 展开,我们发现只要在 DP 的时候同时维护 \(\sum c,\sum c^2,\ldots,\sum c^k\) 即可统计最后的答案

这样就有了个 \(O(n^2k)\) 的做法,上机实现了一下发现是能过大样例的,因此考虑优化该做法

注意到我们最后只关心 \(\sum f_{1,\cdot}\),因此不妨直接令 \(g_i=\sum f_{i,\cdot}\),手玩上面的转移式会发现等价于 \(g_i=g_{i+1}\times [(n-i+1)^a+1]\),因此可以直接 \(O(n)\) 计算 DP 部分

最后总复杂度 \(O(nk)\),而且不难发现最后计算的式子非常简单,感觉还有做到带 \(\log\) 的复杂度的可能性?

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=5005,mod=998244353;
int n,k,C[N][N],pw[N][N];
inline void inc(int& x,CI y)
{
    if ((x+=y)>=mod) x-=mod;
}
inline void dec(int& x,CI y)
{
    if ((x-=y)<0) x+=mod;
}
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
    for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
int main()
{
    scanf("%d%d",&n,&k);
    int ans=0,ifact_n=1;
    for (RI i=1;i<=n;++i)
    ifact_n=1LL*ifact_n*quick_pow(i)%mod;
    C[0][0]=1;
    for (RI i=1;i<=k;++i)
    {
        C[i][0]=1;
        for (RI j=1;j<=i;++j)
        {
            C[i][j]=C[i-1][j];
            inc(C[i][j],C[i-1][j-1]);
        }
    }
    for (RI i=1;i<=n;++i)
    {
        pw[i][0]=1;
        for (RI j=1;j<=k;++j)
        pw[i][j]=1LL*pw[i][j-1]*i%mod;
    }
    for (RI p=1;p<=k;++p)
    {
        /*
        for (RI i=1;i<=n;++i)
        for (RI j=i;j<=n;++j)
        f[i][j]=0;
        f[n][n]=1;
        for (RI i=n;i>1;--i)
        for (RI j=i;j<=n;++j)
        {
            inc(f[i-1][i-1],f[i][j]);
            inc(f[i-1][j],1LL*f[i][j]*quick_pow(n-i+1,p)%mod);
        }
        int c=0;
        for (RI j=1;j<=n;++j)
        inc(c,f[1][j]);
        */
        int c=1;
        for (RI i=n;i>1;--i)
        c=1LL*c*(1+pw[n-i+1][p])%mod;
        c=1LL*c*quick_pow(ifact_n,p)%mod;
        if (p&1) inc(ans,1LL*C[k][p]*c%mod);
        else dec(ans,1LL*C[k][p]*c%mod);
    }
    return printf("%d\n",ans),0;
}

J. Subrectangle Count

队友讨论和写的这题,我题目都没看

#include <bits/stdc++.h>

using llsi = long long;

template<typename T>
struct Trie {
    Trie *go[2];
    T info;
    // void* operator new(size_t size) { return std::malloc(size); }
    // void operator delete(void* p) { free(p); }
    Trie(): go{nullptr, nullptr}, info{0} {}
    ~Trie() { for(auto ch: go) if(ch) delete ch; }
    T& insert(llsi val, llsi masklen) {
        auto cur = this;
        while(masklen--) {
            llsi u = val & 1; val >>= 1;
            if(!cur->go[u]) cur->go[u] = new Trie();
            cur = cur->go[u];
        }
        return cur->info;
    }

    // void insert_plus(llsi val, llsi masklen) {
    //     auto cur = this;
    //     while(masklen--) {
    //         cur->info += 1;
    //         llsi u = val & 1; val >>= 1;
    //         if(!cur->go[u]) cur->go[u] = new Trie();
    //         cur = cur->go[u];
    //     }
    //     cur->info += 1;
    // }

    llsi query(llsi val, llsi masklen)  {
        auto cur = this;
        llsi res = 0;
        while(masklen--) {
            res += cur->info;
            llsi u = val & 1; val >>= 1;
            if(!cur->go[u]) return 0;
            cur = cur->go[u];
        }
        return cur->info;
    }
};

void sumup(Trie<llsi> *cur) {
    for(auto ch: cur->go) if(ch) sumup(ch), cur->info += ch->info;
}

llsi work() {
    llsi n, m;
    auto root0 = new Trie<llsi>();
    auto root1 = new Trie<llsi>();
    std::cin >> n >> m;
    std::vector<llsi> a(n), b(m);
    for(auto &a: a) std::cin >> a;
    for(auto &b: b) std::cin >> b;
    for(int j = 0; j + 1 < m; ++j) {
        if((b[j] ^ b[j + 1]) != 1) continue;
        auto root = ((b[j] & 1) ? root1 : root0);
        root->insert(b[j], 30) += 1;
        // std::println("[Debug INSERT] b[{}] = {}, root{}->insert({})", j, b[j], (b[j] & 1), b[j] >> 1);
    }
    sumup(root0), sumup(root1);
    llsi ans = 0;
    for(int i = 0; i + 1 < n; ++i) {
        auto root = (a[i] & 1) ? root1 : root0;
        if(__builtin_popcountll((a[i] ^ a[i + 1]) + 2) != 1) continue;
        llsi y = __builtin_ctzll((a[i] ^ a[i + 1]) + 2);
        auto upd = root->query(((1ll << (y - 1)) - 2) ^ (a[i]), y);
        // std::println("[Debug] a[{}] = {}, y = {}, qry = {}, upd = {}", i, a[i], y, ((1ll << (y - 1)) - 2) ^ (a[i]), upd);
        ans += upd;
    }
    // for(int i = 0; i < n; ++i) for(int j = 0; j < m; ++j) std::cerr << (a[i] ^ b[j]) << char(j == m - 1 ? 10 : 32);
    return ans;
}

int main() {
    std::ios::sync_with_stdio(false);
    int T; std::cin >> T; while(T--) std::cout << work() << char(10);
    return 0;
}

M. Replacement

手玩一下会发现断点只可能是 \(2,n-1\),或者从 \(3\) 往后的第一个 \(1\) 的左侧这几种情况

确定断点后手动模拟一下取个最优解即可

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

int n;
string str;

string cut(int pos) {
    string ret = "";
    int len = max(pos, n-1-pos);
    for (int i=0; i<len; ++i) {
        int a = (pos-len+i < 0 ? 0 : str[pos-len+i]-'0');
        int b = (n-len+i <= pos ? 0 : str[n-len+i]-'0');
        if (ret.size() == 0 && (a^b)==0) continue;
        ret += ('0'+(a^b));
    }
    if (ret.size() == 0) ret = "0";
    return ret;
}

bool cmp(const string &a, const string &b) {
    if (a.length() != b.length()) return a.length() < b.length();
    else return a < b;
}

void solve() {
    cin >> str;
    n = str.length();
    int pos=n-2;
    for (int i=2; i<n; ++i) {
        if (str[i] == '1') {
            pos = i-1;
            break;
        }
    }
    string sa = cut(n-2), sb = cut(pos), sc = cut(1);
    cout << (cmp(sa, sb) ? (cmp(sb, sc) ? sc : sb) : (cmp(sa, sc) ? sc : sa)) << "\n";
}

signed main() {
    int T; cin >> T; while (T--) solve();
    return 0;
}

Postscript

在 ECF 之前尽量保证每周都训练吧,这次不开玩笑真是 Last Dance 了

posted @ 2025-12-14 17:38  空気力学の詩  阅读(101)  评论(0)    收藏  举报