第十一届中国大学生程序设计竞赛 郑州站(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 了

浙公网安备 33010602011771号