第十一届中国大学生程序设计竞赛 济南站(CCPC 2025 Jinan Site)
Preface
还是上上周的 CCPC 济南,当时场外看同校的 UESTC_有无名队 拿下亚军(可惜最后几分钟被 PKU 反杀了,不然真冠军了),也对这场比赛的毒瘤程度早有耳闻
实际打了之后发现前中期题的 GAP 非常大,A 题感觉很不 match 我们队的知识点,导致赛后半小时才搞出来
第五题的 E 和第四题的 F 之间也卡了两个多小时,总而言之就是做大牢,最后也是连金线都摸不到,苦露西
A. 暗语
神秘 Crypto 题,然而我们队纯靠打表找规律硬做的这个题
需要注意到循环节和 __builtin_ctz 有关,还要猜出从模 \(2^{p-1}\) 扩展到模 \(2^p\) 的增量,我只能说这题是真有点 ex 的
唉,好羡慕数学大手子
#include <bits/stdc++.h>
using llui = __int128_t;
std::istream& operator >>(std::istream& in, llui &a) {
long long unsigned int _a; in >> _a; a = _a;
return in;
}
std::ostream& operator <<(std::ostream& out, llui a) {
long long unsigned int _a = a; out << _a;
return out;
}
llui get_mask(llui mask_len) {
// if(mask_len >= 64) return -llui(1);
return (llui(1) << mask_len) - llui(1);
}
llui ksm(llui a, llui b, llui mask_len = 64) {
llui c = llui(1);
while(b) {
if(b & llui(1)) c = c * a;
a = a * a;
b >>= llui(1);
}
if(mask_len == 64) return c;
return c & ((llui(1) << mask_len) - llui(1));
}
std::optional<llui> inv(llui a, llui p) {
if(a % 2 == 0) return std::nullopt;
return ksm(a, (llui(1) << (p - llui(1))) - llui(1), p);
}
std::optional<llui> solve(llui a, llui b, llui p) {
auto debug = [&](llui ans) {
// std::cerr << "solve(" << a << ", " << b << ", " << p << ") = " << ans << char(10);
return ans;
};
if(b % (llui(1) << p) == 1) return debug(0ull);
if(a % 2 == 0 || p <= 3) {
llui A = 1, x = 0;
while(x <= 64) {
if(((A - b) & get_mask(p)) == 0) return debug(x);
A *= a, x += llui(1);
}
return std::nullopt;
}
if(b % 2 == 0) return std::nullopt;
if(p == 1) return debug(llui(1));
// if(((a - b) & get_mask(p)) == 0) return debug(llui(1));
auto _x = solve(a, b, p - 1);
if(!_x) return std::nullopt;
llui P = (llui(1) << p - 2 - __builtin_ctzll((a % (llui(1) << p) + 1) / 4));
if(a == 1) P = 1;
if(a == get_mask(p)) P = 2;
llui x = *_x;
if(P != 1) x %= P;
if(((ksm(a, x, p) - b) & get_mask(p)) == 0ull) return debug(x);
llui nx = (x < P / 2 ? x + P / 2 : x - P / 2);
if(((ksm(a, nx, p) - b) & get_mask(p)) == 0ull) return debug(nx);
// std::cerr << "x = " << x << ", a = " << (a & get_mask(p)) << ", b = " << (b & get_mask(p)) << ", P = " << P << char(10);
return std::nullopt;
}
int main(void) {
std::ios::sync_with_stdio(false);
// for(llui a = 1; a <= 31; a += 2) {
// std::cerr << std::setw(2) << std::right << a << "(" << (a + 1) / 4 << ", " << (1 << 3 - __builtin_ctz((a + 1) / 4)) << "): ";
// for(llui b = 1; b <= 8; ++b)
// std::cerr << std::setw(2) << std::right << ksm(a, b, 5) << char(b == 8 ? 10 : 32);
// }
// for(llui a = 1; a <= 63; a += 2) {
// std::cerr << std::setw(2) << std::right << a << "(" << std::setw(2) << std::right << (a + 1) / 4 << ", " << std::setw(2) << std::right << (1 << 4 - __builtin_ctz((a + 1) / 4)) << "): ";
// for(llui b = 1; b <= 16; ++b)
// std::cerr << std::setw(2) << std::right << ksm(a, b, 6) << char(b == 16 ? 10 : 32);
// }
// return 0;
// for(llui i = 1; i <= 63; i += 2) for(llui p = 1; p <= 64; ++p) {
// auto iv = inv(i, p);
// if(iv) std::cerr << *iv << "(" << (*iv * i & (p >= 64 ? -1 : (llui(1) << p) - 1)) << ")";
// else std::cerr << "N/A";
// std::cerr << char(p == 64 ? 10 : 32);
// }
int T; std::cin >> T; while(T--) {
llui a, b;
std::cin >> a >> b;
auto ans = solve(a, b, 64);
if(ans) std::cout << *ans << char(10);
else std::cout << "broken message\n";
}
return 0;
}
C. 查找关键词
签到,按顺序匹配关键词序列,对于当前的每个值,找在上次值出现位置之后且最靠前的即可
实现时不需要二分,直接用单调性找即可
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
const int N=1e6+5,M=15;
int m,b[M],t,n; vector <int> pos[M];
int main()
{
scanf("%d",&m);
for (RI i=1;i<=m;++i)
scanf("%d",&b[i]);
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=1;i<=m;++i) pos[i].clear();
for (RI i=1;i<=n;++i)
{
int x; scanf("%d",&x);
pos[x].push_back(i);
}
for (RI i=1;i<=m;++i)
reverse(pos[i].begin(),pos[i].end());
int ans=0;
for (;;)
{
int lst=0,flag=1;
for (RI i=1;i<=m;++i)
{
int x=b[i];
while (!pos[x].empty()&&lst>pos[x].back()) pos[x].pop_back();
if (pos[x].empty()) { flag=0; break; }
lst=pos[x].back(); pos[x].pop_back();
}
if (!flag) break; ans+=flag;
}
printf("%d\n",ans);
}
return 0;
}
E. 二分图问题
考虑如果树上存在两个不交的连通块 \(S,T\),则它们之间一定存在一条路径(不包含端点),满足删去这条路径上的任意一个点/边后 \(S,T\) 分属不同连通块
刚开始想的是通过对不同长度的路径容斥进行处理,但后面发现非常难搞,遂考虑更进一步的转化
考虑这个题在删除一个点/边后的方案数是好计算的,而一条不包含两端点的路径满足边数减去点数恒等于 \(1\)
因此我们可以考虑用删去每条边后,在两部分找两个连通块的方案数,减去删去每个点后,找两个连通块的方案数
分析一下会发现此时只要对每个点求出 in[x],out[x] 表示子树内/外选同色点集的方案数即可,用 DSU on tree 即可快速统计
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005,mod=998244353;
int t,n,pw2[N],c[N],sz[N],son[N],in[N],out[N],all[N],bkt[N],IN,OUT; vector <int> v[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 void DFS1(CI now=1,CI fa=0)
{
sz[now]=1;
for (auto to:v[now])
{
if (to==fa) continue;
DFS1(to,now); sz[now]+=sz[to];
if (sz[to]>sz[son[now]]) son[now]=to;
}
}
inline void add(int x)
{
x=c[x];
dec(IN,pw2[bkt[x]]-1); dec(OUT,pw2[all[x]-bkt[x]]-1);
++bkt[x];
inc(IN,pw2[bkt[x]]-1); inc(OUT,pw2[all[x]-bkt[x]]-1);
}
inline void del(int x)
{
x=c[x];
dec(IN,pw2[bkt[x]]-1); dec(OUT,pw2[all[x]-bkt[x]]-1);
--bkt[x];
inc(IN,pw2[bkt[x]]-1); inc(OUT,pw2[all[x]-bkt[x]]-1);
}
inline void travel_add(CI now,CI fa)
{
add(now); for (auto to:v[now])
if (to!=fa) travel_add(to,now);
}
inline void travel_del(CI now,CI fa)
{
del(now); for (auto to:v[now])
if (to!=fa) travel_del(to,now);
}
inline void DSU(CI now=1,CI fa=0,CI flag=1)
{
for (auto to:v[now])
{
if (to==fa||to==son[now]) continue;
DSU(to,now,0);
}
if (son[now]) DSU(son[now],now,1);
for (auto to:v[now])
{
if (to==fa||to==son[now]) continue;
travel_add(to,now);
}
add(now); in[now]=IN; out[now]=OUT;
if (!flag) travel_del(now,fa);
}
inline void DFS2(int& ans,CI now=1,CI fa=0)
{
if (now!=1) inc(ans,2LL*in[now]*out[now]%mod);
int sum=out[now],quad=1LL*out[now]*out[now]%mod;
for (auto to:v[now])
{
if (to==fa) continue;
DFS2(ans,to,now);
inc(sum,in[to]); inc(quad,1LL*in[to]*in[to]%mod);
}
sum=1LL*sum*sum%mod; dec(sum,quad);
dec(ans,sum);
}
int main()
{
pw2[0]=1;
for (RI i=1;i<=200000;++i) pw2[i]=2LL*pw2[i-1]%mod;
for (scanf("%d",&t);t;--t)
{
scanf("%d",&n);
for (RI i=1;i<=n;++i) all[i]=bkt[i]=0;
for (RI i=1;i<=n;++i)
{
scanf("%d",&c[i]); ++all[c[i]];
son[i]=0; v[i].clear();
}
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);
}
IN=OUT=0;
for (RI i=1;i<=n;++i) inc(OUT,pw2[all[i]]-1);
DFS1(); DSU();
// for (RI i=1;i<=n;++i)
// printf("i = %d, in[i] = %d, out[i] = %d\n",i,in[i],out[i]);
int ans=0; DFS2(ans);
printf("%d\n",ans);
}
return 0;
}
F. 方格填数
注意到题目要求最小,因此让每一段能断则断一定是最优的
一个关键的观察是:对于 \(r_i-l_i+1\ge 3\) 的位置,一定可以做为一个单独的隔断点,因为它们至少能和左右两边保持不同
因此这题实际上只要考虑取值唯一和两种取值的情况,做一个简单的 DP 即可
这个 DP 是把端点以及选哪个数作为状态,二元组总贡献和上一段的长度作为存储的信息
虽然乍一看这个 DP 状态数可能会爆,但利用经典单调性剔除掉无用的二元组后,交上去就直接过了,非常的神奇
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=1e6+5,INF=1e18;
int t,n,l[N],r[N]; vector <pi> f[N][2];
inline int solve(CI beg,CI end)
{
if (beg>end) return 0;
for (RI i=beg;i<=end;++i)
f[i][0].clear(),f[i][1].clear();
f[beg][0].push_back({1,1});
if (l[beg]!=r[beg]) f[beg][1].push_back({1,1});
for (RI i=beg+1;i<=end;++i)
{
auto upt=[&](CI idx,vector <pi>& vec,CI num)
{
vector <pi> tmp;
for (RI j=0;j<=1;++j)
{
if (f[idx-1][j].empty()) continue;
for (auto [val,len]:f[idx-1][j])
{
if (num!=l[idx-1]+j)
{
tmp.push_back({val+1,1});
} else
{
tmp.push_back({val+2*len+1,len+1});
}
}
}
sort(tmp.begin(),tmp.end());
int lst_len=INF;
for (auto [val,len]:tmp)
{
if (len>=lst_len) continue;
vec.push_back({val,len});
lst_len=len;
}
};
upt(i,f[i][0],l[i]);
if (l[i]!=r[i]) upt(i,f[i][1],r[i]);
}
int res=INF;
for (RI j=0;j<=1;++j)
{
for (auto [val,len]:f[end][j])
res=min(res,val);
}
return res;
}
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld",&n);
for (RI i=1;i<=n;++i) scanf("%lld",&l[i]);
for (RI i=1;i<=n;++i) scanf("%lld",&r[i]);
int lst=0,ans=0;
for (RI i=1;i<=n;++i)
if (r[i]-l[i]+1>=3) ans+=solve(lst+1,i-1)+1,lst=i;
ans+=solve(lst+1,n);
printf("%lld\n",ans);
}
return 0;
}
K. 开火车
这题队友讨论出的,我题目都没看
#include<bits/stdc++.h>
using namespace std;
const int N = 5e5+5;
int n, A[N], B[N], cnt[N], pir[N];
void solve() {
cin >> n;
for (int i=1; i<=n; ++i) cnt[i] = 0, B[i] = -1, pir[i]=-1;
for (int i=1; i<=n; ++i) cin >> A[i], ++cnt[A[i]];
vector<int> vec;
for (int i=1; i<=n; ++i) {
if (0==cnt[i]) vec.push_back(i);
}
for (int i=1; i<=n; ++i) {
if (cnt[A[i]]==2 && A[i] != A[1]) {
if (-1 == pir[A[i]]) {
int x = vec.back(); vec.pop_back();
pir[A[i]] = x;
B[i-1] = x;
} else {
B[i-1] = pir[A[i]];
}
}
}
for (int i=n, j=n; i>0 && j>0; --i, --j) {
while (i>0 && cnt[A[i]]>1) --i;
while (j>=i && B[j]!=-1) --j;
if (i>0) B[j] = A[i];
}
// printf("B:"); for (int i=1; i<=n; ++i) printf("%d ", B[i]); puts("");
int ans = 0;
if (vec.size() > 0) {
ans = 1;
for (int i=1; i<=n; ++i) if (B[i]==-1) B[i] = vec[0];
}
cout << ans << '\n';
for (int i=1; i<=n; ++i) cout << B[i] << (i==n ? '\n' : ' ');
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int T; cin >> T; while (T--) solve();
return 0;
}
L. 列队
考虑把每次 PK 的两个人留下的人放到队列末尾,则最后得到一个长 \(2n-1\) 的序列
可以很容易地维护出修改 \(i\in [1,n]\) 的位置时,会对 \(j\in[n+1,2n-1]\) 的哪些位置产生影响,并且影响的位置数量是 \(O(\log n)\) 的
因此拿一个树状数组暴力维护修改即可,总复杂度 \(O(n\log^2 n)\)
#include<cstdio>
#include<iostream>
#include<vector>
#include<set>
#define RI register int
#define CI const int&
using namespace std;
const int N=200005;
int n,m,ls[N],rs[N],out[N],rem[N];
set <int> idx[N]; vector <int> pos[N];
class Tree_Array
{
private:
long long bit[N];
public:
#define lowbit(x) (x&-x)
inline void add(RI x,CI y)
{
for (;x<n;x+=lowbit(x)) bit[x]+=y;
}
inline long long get(RI x,long long res=0)
{
for (;x;x-=lowbit(x)) res+=bit[x]; return res;
}
#undef lowbit
}BIT;
int main()
{
scanf("%d%d",&n,&m);
for (RI i=1;i<=n;++i)
{
scanf("%d",&rem[i]);
idx[i].insert(i);
}
auto upt=[&](CI x)
{
out[x]=max(rem[ls[x]],rem[rs[x]]);
rem[x]=min(rem[ls[x]],rem[rs[x]]);
};
for (RI i=n+1,j=1;i<2*n;++i)
{
auto merge=[&](set <int> A,set <int> B)
{
if ((int)A.size()<(int)B.size()) swap(A,B);
for (auto x:B) A.insert(x);
return A;
};
idx[i]=merge(idx[j],idx[j+1]);
ls[i]=j; rs[i]=j+1; upt(i);
j+=2;
}
for (RI i=1;i<n;++i)
{
for (auto x:idx[n+i]) pos[x].push_back(i);
BIT.add(i,out[n+i]);
}
while (m--)
{
char opt[5]; int x,y;
scanf("%s%d%d",opt,&x,&y);
if (opt[0]=='C')
{
swap(rem[x],rem[y]);
for (auto p:pos[x])
{
BIT.add(p,-out[n+p]);
upt(n+p);
BIT.add(p,out[n+p]);
}
for (auto p:pos[y])
{
BIT.add(p,-out[n+p]);
upt(n+p);
BIT.add(p,out[n+p]);
}
} else printf("%lld\n",BIT.get(y)-BIT.get(x-1));
}
return 0;
}
Postscript
这周末还有一场 CCPC 重庆要现场打,虽然按道理说去年已经拿到了 CCPC 的 Au,今年也有幸参加了 CCPC Final
但不管怎么说还是尽量往好的成绩努力吧,今年的两场 ICPC Regional 队友都实力带飞我拿了 Au,下场牢闪能不能发发力啊

浙公网安备 33010602011771号