第十一届中国大学生程序设计竞赛 哈尔滨站(CCPC 2025 Harbin Site)
Preface
上周的 CCPC 哈尔滨,同时间进行的 ICPC 南京又混了个金尾,只能说狗运这一块
哈尔滨的题感觉和去年郑州比较像,基本上大家都是 7~8 个题然后拼手速,只可惜这次被 Adhoc 题 I 防了 4h 最后中罚时 7 题喜提银首
这么看来去年郑州的高位金完全是运气爆棚啊,CCPC 拿金确实还是太困难了
A. k-子集和最大公约数问题
要点脑子的签到,第一个答案就是所有元素差值的 \(\gcd\),第二个答案要除以初始时所有数的 \(\gcd\)
#include<cstdio>
#include<iostream>
#include<algorithm>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=100005;
int t,n,a[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld",&n);
int G=0,D=0;
for (RI i=1;i<=n;++i)
{
scanf("%lld",&a[i]);
G=__gcd(G,a[i]);
if (i>1) D=__gcd(D,abs(a[i]-a[i-1]));
}
if (D==0) { puts("infinite"); continue; }
printf("%lld %lld\n",D,D/G);
}
return 0;
}
B. 液压机
模拟,不难发现 \(y\) 坐标是没有任何影响的,可以忽略
在 \(x\) 方向上的运动过程可以分讨,先特判掉两个墙都不动的情况
否则由于两面墙之间的距离会以最少 \(\frac{1}{20}\) 的比例幂次增长,小球碰撞的次数是 \(\log\) 级别的,可以暴力模拟之
#include<cstdio>
#include<iostream>
#include<cmath>
#define RI register int
#define CI const int&
typedef long double LDB;
const LDB EPS=1e-9;
int cases; LDB x,y,vx,vy,Y1,y2,vy1,vy2,x1,x2,vx1,vx2;
inline int sgn(const LDB& x)
{
return std::fabs(x)<EPS?0:(x<0?-1:1);
}
int main()
{
for (scanf("%d",&cases);cases;--cases)
{
std::scanf("%Lf%Lf%Lf%Lf",&x,&y,&vx,&vy);
std::scanf("%Lf%Lf%Lf%Lf",&Y1,&y2,&vy1,&vy2);
std::scanf("%Lf%Lf%Lf%Lf",&x1,&x2,&vx1,&vx2);
LDB T=(y2-Y1)/(vy1+vy2),X,Y=Y1+T*vy1;
if (sgn(vx1)==0&&sgn(vx2)==0)
{
LDB dt=std::fabs(x1-x2)/std::fabs(vx);
// printf("T = %.12Lf, dt = %.12Lf\n",T,dt);
T=T-std::floor(T/(2.0L*dt)+EPS)*(2.0L*dt);
while (sgn(T)>0)
{
LDB t,xt;
if (sgn(vx)>0) xt=x2; else xt=x1;
t=std::min(std::fabs(xt-x)/std::fabs(vx),T);
x+=vx*t; T-=t;
if (sgn(T)>0) vx=-vx;
}
X=x;
} else
{
while (sgn(T)>0)
{
LDB t,xt,vt;
if (sgn(vx)>0) xt=x2,vt=vx2; else xt=x1,vt=vx1;
if (sgn(std::fabs(vx)-vt)<=0) t=T;
else t=std::min(std::fabs(xt-x)/(std::fabs(vx)-vt),T);
x+=vx*t; x1-=vx1*t; x2+=vx2*t; T-=t;
if (sgn(T)>0) vx=-vx;
}
X=x;
}
printf("%.12Lf %.12Lf\n",X,Y);
}
return 0;
}
G. 扫雪
考虑从下往上,从右往左扫描整个矩阵
对于第 \(i\) 列,用 \(bins_i\) 记录下这一列还可以容纳多少雪
如果当前遇到的 \(a_{i,j}\) 是负的,就直接放进 \(bins_j\) 中;否则考虑将其消除
对于 \(k\in [j,m]\),依次扫描每个 \(bins_k\),尽量把雪移动过去即可
实现时只需要用一个栈维护所有还能容纳雪的 \(bins\) 位置即可,注意如果一个正的 \(a_{i,j}\) 消除不完就应该直接丢弃
#include<cstdio>
#include<iostream>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
const int N=1005;
int t,n,m,a[N][N],bins[N];
signed main()
{
for (scanf("%lld",&t);t;--t)
{
scanf("%lld%lld",&n,&m);
long long sum=0;
for (RI i=1;i<=n;++i)
for (RI j=1;j<=m;++j)
scanf("%lld",&a[i][j]),sum+=abs(a[i][j]);
for (RI j=1;j<=m;++j) bins[j]=0;
for (RI i=n;i>=1;--i)
{
static int stk[N]; int top=0;
for (RI j=m;j>=1;--j)
{
if (a[i][j]<0) bins[j]+=a[i][j];
if (bins[j]<0) stk[++top]=j;
while (top>0&&a[i][j]>0)
{
int dlt=min(a[i][j],-bins[stk[top]]);
sum-=2LL*dlt;
a[i][j]-=dlt; bins[stk[top]]+=dlt;
if (bins[stk[top]]==0) --top;
}
}
}
printf("%lld\n",sum);
}
return 0;
}
I. 六边形翻转
关了我们队三人几乎一整场,太有人类智慧了
首先这题等价于将两个图的颜色异或,然后判断得到的图能否全部删成白色
把样例画出来会发现一个很牛的性质:三个方向上的每条线上,黑色块的数量都是偶数
后面尝试思考了下发现这个性质很对,因为一次操作并不会改变上面这个东西,然后猜了一发交上去就过了
#include<cstdio>
#include<iostream>
#include<map>
#define RI register int
#define CI const int&
using namespace std;
int t,n,m;
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&m);
map <int,int> X,Y,Z;
for (RI i=1;i<=n+m;++i)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
X[x]^=1; Y[y]^=1; Z[z]^=1;
}
bool flag=1;
for (auto [_,val]:X) if (val!=0) flag=0;
for (auto [_,val]:Y) if (val!=0) flag=0;
for (auto [_,val]:Z) if (val!=0) flag=0;
puts(flag?"YES":"NO");
}
return 0;
}
J. 幻想乡的裁判长
徐神 solo 的字符串,好像是个 Manacher 分讨
#include <bits/stdc++.h>
constexpr int $n = 20'000'007;
namespace manacher {
static int R[$n];
template<typename F>
void odd(char *s, int n, F cb) {
s[0] = 1, s[n + 1] = 0;
for(int i = 1, m = 0, r = 0, k, l; i <= n; ++i) {
if(i <= r) {
k = m * 2 - i;
if(i + R[k] <= r) {
R[i] = R[k];
cb(i - R[i] + 1, i + R[i] - 1);
continue;
}
} else r = i;
m = i, l = m * 2 - r;
while(s[l] == s[r]) --l, ++r;
R[i] = r - i; cb(l + 1, --r);
}
}
template<typename F>
void even(char *s, int n, F cb) {
s[0] = 1, s[n + 1] = 0;
for(int i = 1, m = 0, r = 0, k, l; i <= n; ++i) {
if(i <= r) {
k = m * 2 - i;
if(i + R[k] <= r) {
R[i] = R[k];
if(R[i]) cb(i - R[i], i + R[i] - 1);
continue;
}
} else r = i;
m = i, l = m * 2 - r - 1;
while(s[l] == s[r]) --l, ++r;
R[i] = r - i; r -= 1; l += 1;
if(r > l) cb(l, r);
}
}
} // namespace manacher
int n, ans, ans_l, ans_r;
char org_s[$n], s[$n];
int map[$n], prevNotW[$n], nextNotW[$n];
inline bool isLeftBound(int pos) { return map[pos - 1] == map[pos] - 1; }
inline bool isRightBound(int pos) { return map[pos + 1] == map[pos] + 1; }
void update_ans(int l, int r) {
if(l > r) return ;
if(r - l + 1 > ans) {
ans = r - l + 1;
ans_l = l;
ans_r = r;
}
}
void callback(int l, int r) {
// std::cerr << "callback(" << l << ", " << r << ")\n";
int org_l = map[l], org_r = map[r];
// if(l == 5 && r == 8) {
// std::cerr << "org_l, org_r = " << org_l << ", " << org_r << char(10);
// std::cerr << isLeftBound(l) << ", " << isRightBound(r) << char(10);
// }
assert(isLeftBound(l) || isRightBound(r));
if(isLeftBound(l) && isRightBound(r)) return update_ans(org_l, org_r);
if( !isLeftBound(l) && org_s[org_r] == 'v') return update_ans(org_l + 1, org_r - 1);
if(!isRightBound(r) && org_s[org_l] == 'v') return update_ans(org_l + 1, org_r - 1);
int skip = std::min(nextNotW[org_l] - org_l, org_r - prevNotW[org_r]);
if( isLeftBound(l) && org_s[org_r - skip] == 'v' ) return update_ans(org_l + skip, org_r - skip - 1);
if( isLeftBound(l) && org_s[org_r - skip] == 'w' && org_s[org_l + skip] == 'v') return update_ans(org_l + skip + 1, org_r - skip - 1);
if(isRightBound(r) && org_s[org_l + skip] == 'v' ) return update_ans(org_l + skip + 1, org_r - skip);
if(isRightBound(r) && org_s[org_l + skip] == 'w' && org_s[org_r - skip] == 'v') return update_ans(org_l + skip + 1, org_r - skip - 1);
}
void work() {
scanf("%d", &n);
scanf("%s", org_s + 1);
int p = 1;
for(int i = 1; i <= n; ++i) {
if(org_s[i] == 'w') {
s[p] = s[p + 1] = 'v';
map[p] = map[p + 1] = i;
p += 2;
} else {
s[p] = org_s[i];
map[p] = i;
p += 1;
}
}
map[0] = 0; map[p] = n + 1;
// std::cerr << "s.len = " << p - 1 << char(10);
prevNotW[0] = 0;
for(int i = 1; i <= n; ++i) {
if(org_s[i] == 'w') prevNotW[i] = prevNotW[i - 1];
else prevNotW[i] = i;
}
nextNotW[n + 1] = n + 1;
for(int i = n; i >= 1; --i) {
if(org_s[i] == 'w') nextNotW[i] = nextNotW[i + 1];
else nextNotW[i] = i;
}
ans = 0;
manacher::odd(s, p - 1, callback);
manacher::even(s, p - 1, callback);
// std::cerr << "ans_l, ans_r = " << ans_l << ", " << ans_r << char(10);
for(int i = ans_l; i <= ans_r; ++i) putchar(org_s[i]);
putchar(10);
return ;
}
int main() {
int T; scanf("%d", &T);
while(T--) work();
return 0;
}
K. 01 背包
又是神秘 Guess 题,但这个比较对我胃口一下就猜出来了
由于题面要求对任意 \(W\) 都要满足,因此直接大力猜测一手物品数量 \(n=W\),并且体积满足 \(w_i=i\)
现在难点就是要分配价值,根据样例先假设 \(w_1=x,w_2=x+1\)
接下来每加入一个物品 \(i\),我们都要让最优解选择物品 \(i\) 而贪心不选择物品 \(i\)
简单推一推会有 \(w_3=2x+2,w_4=3x+3,\dots,w_i=(i-1)\times (x+1)\)
为了卡贪心需要让 \(w_1\) 的单价严格最高,因此有 \(x>(n-1)\times (x+1)\),解得 \(x_{\min}=n\),代入式子输出即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int main()
{
int Wm; scanf("%d",&Wm);
int n=Wm; printf("%d\n",n);
for (RI i=1;i<=n;++i)
printf("%d%c",i," \n"[i==n]);
int x=Wm;
for (RI i=1;i<=n;++i)
{
int val=(i==1?x:(i-1)*(x+1));
printf("%d%c",val," \n"[i==n]);
}
return 0;
}
L. 网格避障
签到,直接枚举所有状态做一个按列 DP 即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,INF=1e9;
int t,n,m,k,r[N],c[N],valid[N][N],f[N][N];
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d%d",&n,&m,&k);
for (RI i=0;i<k;++i)
scanf("%d%d",&r[i],&c[i]);
for (RI mask=0;mask<(1<<k);++mask)
{
for (RI i=1;i<=n;++i)
for (RI j=1;j<=m;++j)
{
valid[i][j]=1;
f[i][j]=INF;
}
for (RI i=0;i<k;++i)
if ((mask>>i)&1)
{
for (RI j=1;j<=r[i];++j)
valid[j][c[i]]=0;
} else
{
for (RI j=r[i];j<=n;++j)
valid[j][c[i]]=0;
}
for (RI i=1;i<=n;++i) f[i][1]=0;
for (RI j=1;j<=m;++j)
{
for (RI i=1;i<=n;++i)
if (valid[i][j]) f[i][j]=min(f[i][j],f[i][j-1]+1);
for (RI i=2;i<=n;++i)
if (valid[i][j]) f[i][j]=min(f[i][j],f[i-1][j]+1);
for (RI i=n-1;i>=1;--i)
if (valid[i][j]) f[i][j]=min(f[i][j],f[i+1][j]+1);
}
int ans=INF;
for (RI i=1;i<=n;++i) ans=min(ans,f[i][m]);
printf("%d%c",ans==INF?-1:ans," \n"[mask==(1<<k)-1]);
}
}
return 0;
}
M. 连通的正三角形
比赛最后时刻疯狂 Rush,结果没想到容斥
首先可以把问题转化为统计三元组 \((i,j,k)\) 的个数,其中:
- \(a_i=b_j=c_k\)
- \(i+j\ge n,i+k\ge n,j+k\ge n\)
- \(i+j+k\ne 2n\)(\(<2n\) 是正着的三角形,\(>2n\) 的是反着的,\(=2n\) 会退化)
第一条直接枚举钦定当前算 \(0\) 还是算 \(1\) 的,第二条可以枚举 \(i\) 然后用后缀和快速算 \(j,k\) 的贡献
仔细分析我们会发现一个牛逼的性质,那就是 \(i+j+k=2n\) 天然满足第二条性质,因此我们只要用之前的数量减去违反第三天的数量即可
具体实现时可以 bitset 也可以 FFT,形式已经非常裸了
#include <bits/stdc++.h>
using llsi = long long;
const int N=8e5+5;
namespace Poly {
using LDB = long double;
const LDB PI=acosl(-1);
struct Complex
{
LDB x,y;
inline Complex(LDB x=0, LDB y=0): x(x), y(y) {}
inline Complex conj(void)
{
return Complex(x,-y);
}
friend inline Complex operator + (const Complex& A,const Complex& B)
{
return Complex(A.x+B.x,A.y+B.y);
}
friend inline Complex operator - (const Complex& A,const Complex& B)
{
return Complex(A.x-B.x,A.y-B.y);
}
friend inline Complex operator * (const Complex& A,const Complex& B)
{
return Complex(A.x*B.x-A.y*B.y,A.x*B.y+A.y*B.x);
}
}; int lim,p,rev[N];
inline void init(int n)
{
for (lim=1,p=0;lim<=n;lim<<=1,++p);
for (int i=0;i<lim;++i) rev[i]=(rev[i>>1]>>1)|((i&1)<<p-1);
}
inline void FFT(Complex *f,int opt)
{
for (int i=0;i<lim;++i) if (i<rev[i]) std::swap(f[i],f[rev[i]]);
for (int i=1;i<lim;i<<=1)
{
Complex D(cosl(PI/i),opt*sinl(PI/i));
for (int j=0;j<lim;j+=(i<<1))
{
Complex W(1,0);
for (int k=0;k<i;++k,W=W*D)
{
Complex x=f[j+k],y=W*f[i+j+k];
f[j+k]=x+y; f[i+j+k]=x-y;
}
}
}
if (opt==-1)
{
for (int i=0;i<lim;++i) f[i].x/=lim,f[i].y/=lim;
}
}
}
using namespace Poly;
int n;
std::string s1, s2, s3;
llsi work() {
static Complex A[N],C[N];
init(2*n);
for (int i=0;i<lim;++i) A[i]=C[i]=Complex();
for (int i=1;i<=n;++i)
{
if (s1[i]=='0') A[i].x+=1.0;
if (s3[i]=='0') C[i].x+=1.0;
}
FFT(A,1); FFT(C,1);
for (int i=0;i<lim;++i) A[i]=A[i]*C[i];
FFT(A,-1);
static llsi sfx_a[N],sfx_c[N];
for (int i=n;i>=1;--i)
{
sfx_a[i]=sfx_a[i+1]+(s1[i]=='0');
sfx_c[i]=sfx_c[i+1]+(s3[i]=='0');
}
llsi res=0,sum=(s1[n]=='0')*(s3[n]=='0');
for (int i=1;i<=n;++i)
{
if (i!=n)
{
sum+=(s1[n-i]=='0')*sfx_c[std::max(n-i,i)]+(s3[n-i]=='0')*sfx_a[std::max(n-i,i)];
if (i<=n-i) sum-=(s1[n-i]=='0')*(s3[n-i]=='0');
}
if (s2[i]=='0') res+=sum;
}
for (int i=1;i<=n;++i)
if (s2[i]=='0') res-=(llsi)(A[2*n-i].x+0.5L);
return res;
}
int main() {
std::ios::sync_with_stdio(false);
int T; std::cin >> T;
while(T--) {
std::cin >> n;
std::cin >> s1 >> s2 >> s3;
s1=" "+s1; s2=" "+s2; s3=" "+s3;
llsi ans = work();
// std::cout<<ans<<'\n';
for(auto &c: s1) c = (c ^ 1);
for(auto &c: s2) c = (c ^ 1);
for(auto &c: s3) c = (c ^ 1);
ans += work();
std::cout << ans << char(10);
}
return 0;
}
Postscript
这周末由于要出去上课,队友要去济南旅游,导致没法 VP
下周 ICPC 上海之前可能只有下周三能 VP 一场 ICPC 沈阳 / CCPC 济南了
你问我 ICPC 南京的游记在哪,我只能说有生之年系列了

浙公网安备 33010602011771号