“华为智联杯”无线程序设计大赛暨2025年上海市大学生程序设计竞赛
Preface
摆烂一哥,又好久没训练了
在参加完本校夏令营后顺带就留在学校里了,这两天有空可能会多训练会
这场因为一段时间没训练了,导致前期题写的都不是很快,最后堪堪 9 题,从开场就开始写的 K 到最后也没能写完
A. 序列
由于子序列和顺序无关,因此考虑构造序列 \(c_1,c_2,\dots,c_k\),表示一共用了 \(k\) 种颜色,颜色 \(i\) 的数量是 \(c_i\)
令 \(m=\sum_{i=1}^k c_i\),则序列的价值可以表示为:
注意到当固定 \(m\) 时,这个式子的值范围是 \([2^m-1,m2^{m-1}]\) 的,因此只需要枚举 \(m\le 60\) 的值即可
此时直接钦定 \(c_i\) 单调不降,直接爆搜即可通过,复杂度是和五边形数相关的
#include<cstdio>
#include<iostream>
#include<vector>
#define int long long
#define RI register int
#define CI const int&
using namespace std;
int x,m,pw2[65]; bool flag;
inline void DFS(vector <int>& c,CI left,CI lst,CI sum)
{
if (sum==x&&left==0)
{
flag=1; puts("Yes");
printf("%d\n",m);
for (RI i=0;i<(int)c.size();++i)
for (RI j=1;j<=c[i];++j) printf("%d ",i+1);
return;
}
for (RI i=lst;i<=left;++i)
{
__int128 val=__int128((1LL<<i)-1)*__int128((1LL<<m-i));
// printf("%lld %lld\n",i,val);
if (val+sum>x) continue;
c.push_back(i);
DFS(c,left-i,i,sum+val);
if (flag) return;
c.pop_back();
}
}
signed main()
{
scanf("%lld",&x);
for (m=1;m<=60;++m)
{
vector <int> c;
DFS(c,m,1,0);
if (flag) break;
}
if (!flag) puts("No");
return 0;
}
C. 饺子
感觉发病了写了三分套二分,好在最后卡常硬是卡过去了
先不考虑 \([l,r]\) 的限制,令函数 \(f(x)\) 表示恰好吃了 \(x\) 个饺子能获得的最大收益,显然 \(f(x)\) 是个单峰函数
求出 \(f\) 的极值点 \(k\) 后会发现其实可能的取值只有 \(\{0,m,k,l,r\}\) 这五个,取一个最大的即可
至于如何计算 \(f(x)\),可以考虑二分吃掉的饺子中的最小权值,简单计算一下即可
事实上求极值点 \(k\) 可以直接贪心,复杂度降为一个 \(\log\)
#include<bits/stdc++.h>
#include<assert.h>
using namespace std;
#define int long long
const int N = 1e5+5;
const int INF = (int)1e15+5;
int n, m, val, l, r;
int s[N], a[N], b[N], c[N];
array<int, 3> calc2(int i, int x) {
int score=0, num=0, eqnum = 0;
if (c[i]+a[i] >= x)
{
num++, score+=(c[i]+a[i]);
if (c[i]+a[i]==x) ++eqnum;
}
int num2 = (a[i]-x)/b[i];
num2 = max(0LL, min(s[i]-1, num2));
if (num2==0) return {score,num,eqnum};
if (a[i]-b[i]*num2 == x) ++eqnum;
num += num2;
score += (a[i]-b[i] + a[i]-b[i]*num2) * num2 /2;
return {score, num, eqnum};
}
int calc(int num) {
if (num == 0) return 0;
int L = -2e12, R = 2e6;
while (L <= R) {
int M = (R + L) /2;
// printf("%lld %lld %lld\n",L,R,M);
int xnum = 0, score = 0, eqnum = 0;
for (int i=1; i<=n; ++i) {
auto [aa, bb, cc] = calc2(i, M);
score += aa, xnum += bb, eqnum += cc;
if (xnum > num && xnum - eqnum >= num) break;
}
if (xnum - eqnum < num && num <= xnum) return score-(xnum-num)*M;
if (xnum >= num) L = M+1; else R = M-1;
}
assert(0);
}
int solve() {
cin >> n >> m >> val >> l >> r;
int ssum = 0;
for (int i=1; i<=n; ++i) {
cin >> s[i] >> a[i] >> b[i] >> c[i];
ssum += s[i];
}
int L=0, R=min(m,ssum);
while (R - L > 2) {
int M1 = L + (R - L)/3;
int M2 = R - (R - L)/3;
int val1 = calc(M1);
int val2 = calc(M2);
if (val1>=val2) R = M2; else L = M1;
}
int pos = L;
for (int i=L + 1; i<=R; ++i) {
int val = calc(i);
if (val>calc(pos)) pos = i;
}
// printf("calc(l) = %lld\n",calc(l));
int ans = max(calc(0),calc(min(m,ssum)));
if (ssum<l) return max(ans,calc(pos));
r=min(r,ssum);
ans = max(ans,max(calc(l),calc(r))+val);
if (l<=pos&&pos<=r) ans = max(ans,calc(pos)+val);
else ans = max(ans,calc(pos));
return ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int T; cin >> T; while (T--) cout << solve() << '\n';
return 0;
}
D. 与或博弈
不难发现先手必须一步或零步完成要求,简单讨论下即可
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int t; long long a,b,x,y;
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%lld%lld%lld%lld",&a,&b,&x,&y);
if (a==x&&b==y) { puts("Yes"); continue; }
if (a!=x&&b!=y) { puts("No"); continue; }
if (a==x) puts((b|y)==y?"Yes":"No");
else puts((a&x)==x?"Yes":"No");
}
return 0;
}
E. Djangle 的数据结构
刚开始写了个很 trivial 的东西,query
时只在区间内数全相同时进行在操作,然后喜提 TLE
后面想到个关键优化,不妨维护区间 LCM,如果 \(x\) 是这个值的倍数就无需操作,势能分析一波会发现复杂度很对
#include<cstdio>
#include<iostream>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
typedef long long LL;
const int N=400005,INF=1<<30;
int t,n,q,a[N];
class Segment_Tree
{
private:
int tag[N],G[N],L[N]; LL sum[N];
#define TN CI now=1,CI l=1,CI r=n
#define LS now<<1,l,mid
#define RS now<<1|1,mid+1,r
inline void apply(CI now,CI mv,CI len)
{
tag[now]=G[now]=L[now]=mv; sum[now]=1LL*mv*len;
}
inline void pushdown(CI now,CI ls,CI rs)
{
if (tag[now]) apply(now<<1,tag[now],ls),apply(now<<1|1,tag[now],rs),tag[now]=0;
}
inline void pushup(CI now)
{
sum[now]=sum[now<<1]+sum[now<<1|1];
G[now]=__gcd(G[now<<1],G[now<<1|1]);
if (L[now<<1]>INF||L[now<<1|1]>INF) L[now]=INF+1; else
{
LL val=1LL*L[now<<1]*L[now<<1|1]/__gcd(L[now<<1],L[now<<1|1]);
if (val>INF) L[now]=INF+1; else L[now]=val;
}
}
public:
inline void build(TN)
{
tag[now]=0; if (l==r) return (void)(G[now]=L[now]=sum[now]=a[l]);
int mid=l+r>>1; build(LS); build(RS); pushup(now);
}
inline void modify(CI beg,CI end,CI mv,TN)
{
if (beg<=l&&r<=end) return apply(now,mv,r-l+1); int mid=l+r>>1; pushdown(now,mid-l+1,r-mid);
if (beg<=mid) modify(beg,end,mv,LS); if (end>mid) modify(beg,end,mv,RS); pushup(now);
}
inline LL query(CI beg,CI end,CI x,TN)
{
if (beg<=l&&r<=end)
{
if (x%L[now]==0) return sum[now];
if (G[now]==L[now])
{
apply(now,__gcd(G[now],x),r-l+1);
return sum[now];
}
}
int mid=l+r>>1; LL ans=0; pushdown(now,mid-l+1,r-mid);
if (beg<=mid) ans+=query(beg,end,x,LS); if (end>mid) ans+=query(beg,end,x,RS);
pushup(now); return ans;
}
#undef TN
#undef LS
#undef RS
}SEG;
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&q);
for (RI i=1;i<=n;++i) scanf("%d",&a[i]);
for(SEG.build();q;--q)
{
int opt,l,r,x;
scanf("%d%d%d%d",&opt,&l,&r,&x);
if (opt==0) SEG.modify(l,r,x);
else printf("%lld\n",SEG.query(l,r,x));
}
}
return 0;
}
G. 矩阵
考虑求出第一个大于 \(n\) 的质数 \(P\),经过暴力检验发现 \(P\le n+40\),然后按照如下规则构造即可
#include<cstdio>
#include<iostream>
#include<assert.h>
#define RI register int
#define CI const int&
using namespace std;
const int N=2505;
int n;
inline bool is_prime(CI x)
{
if (x==1) return 0;
for (RI i=2;i*i<=x;++i)
if (x%i==0) return 0;
return 1;
}
int main()
{
/*for (RI i=1;i<=2500;++i)
{
int P;
for (RI j=i+1;;++j)
if (is_prime(j)) { P=j; break; }
assert(P-i<=40);
}*/
scanf("%d",&n);
int P;
for (RI i=n+1;;++i)
if (is_prime(i)) { P=i; break; }
for (RI i=1;i<=n;++i)
for (RI j=1;j<=n;++j)
printf("%d%c",1+(i-1)*P+j-1," \n"[j==n]);
return 0;
}
H. V 我 112.5
签到
#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
int x;
int main()
{
scanf("%dd",&x);
return printf("Vivo %.3lf",50.0*(1.0+1.0*x/100.0)),0;
}
I. 真相
简单 DP,令 \(f_{i,j}\) 表示点 \(i\) 的子树里有 \(j\) 个人说真话
每次转移先用树上背包合并儿子的贡献,然后讨论下当前点的两种情况即可
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 5005;
const int MOD = 998244353;
void inc(int &x, int a) {if ((x+=a)>=MOD) x-=MOD;}
int n, A[N], sz[N];
vector<int> G[N];
int f[N][N], g[N][N];
void dfs(int x, int fa) {
static int tmp[N];
g[x][0] = 1;
sz[x] = 0;
for (int v : G[x]) {
if (v == fa) continue;
dfs(v, x);
for (int i=sz[x]; i>=0; --i) {
for (int j=0; j<=sz[v]; ++j) {
inc(tmp[i+j], g[x][i]*f[v][j]%MOD);
}
}
for (int j=0; j<=sz[x]+sz[v]; ++j) g[x][j] = tmp[j], tmp[j] = 0;
sz[x] += sz[v];
}
sz[x] += 1;
int sumg = 0;
// for (int i=0; i<=sz[x]; ++i) printf("g[%d][%d]=%d\n", x, i, g[x][i]), inc(sumg, g[x][i]);
// printf("sumg=%lld\n", sumg);
for (int i=0; i<=sz[x]; ++i) {
f[x][i] = 0;
if (A[x]==i) {
if (i>0) inc(f[x][i], g[x][i-1]);
} else inc(f[x][i], g[x][i]);
// printf("f[%lld][%lld]=%lld\n", x, i, f[x][i]);
}
}
int solve() {
cin >> n;
for (int i=1; i<=n; ++i) cin >> A[i];
for (int i=1; i<n; ++i) {
int u, v; cin >> u >> v;
G[u].push_back(v); G[v].push_back(u);
}
dfs(1, -1);
// printf("sz:"); for (int i=1; i<=n; ++i) printf("%d ", sz[i]); printf("\n");
int ans = 0;
for (int i=0; i<=n; ++i) inc(ans, f[1][i]);
for (int i=1; i<=n; ++i) G[i].clear();
for (int i=1; i<=n; ++i) for (int j=0; j<=sz[i]; ++j) g[i][j] = 0;
return ans;
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
int T; cin >> T; while (T--) cout << solve() << '\n';
return 0;
}
J. 画圈
先求出只使用黑边时的连通块,然后枚举每条白边,若该边两个端点在同一连通块中显然可以直接产生 \(1\) 的贡献
否则把连通块缩成一个点后,建立一棵白边的生成树,那么所有剩下的非树边都可以产生 \(1\) 的贡献
#include<cstdio>
#include<iostream>
#include<vector>
#define RI register int
#define CI const int&
using namespace std;
typedef pair <int,int> pi;
const int N=200005;
int t,n,m,fa[N];
inline int getfa(CI x)
{
return fa[x]!=x?fa[x]=getfa(fa[x]):x;
}
int main()
{
for (scanf("%d",&t);t;--t)
{
scanf("%d%d",&n,&m);
vector <pi> E[2]; // col=0 -> white; col=1 -> black
for (RI i=1;i<=m;++i)
{
int col,x,y;
scanf("%d%d%d",&x,&y,&col);
E[col].push_back({x,y});
}
for (RI i=1;i<=n;++i) fa[i]=i;
for (auto [x,y]:E[1]) fa[getfa(x)]=getfa(y);
int ans=0; vector <pi> EE;
for (auto [x,y]:E[0])
{
x=getfa(x); y=getfa(y);
if (x==y) ++ans; else EE.push_back({x,y});
}
ans+=(int)EE.size();
for (auto [x,y]:EE)
{
x=getfa(x); y=getfa(y);
if (x!=y) --ans,fa[x]=y;
}
printf("%d\n",ans);
}
return 0;
}
K. 神之一手
这题扔给徐神写了,但超过 \(80\) 的情况一直过不了样例,十分神秘
M. 魔法使考核
不难发现翻倍操作可以转化为全局,因此可以枚举最大能用的翻倍操作数量
考虑单个人时,若当前数是奇数,则必然要用一次加一得来;否则如果还有翻倍操作先用掉一定是最优的
注意爆 long long
的问题
#include <bits/stdc++.h>
using i128 = __int128_t;
using llsi = long long signed int;
llsi calc(llsi x, llsi res) {
llsi cur=0;
while (x>0)
{
if (x&1) ++cur,x^=1;
if (res>0) --res,x>>=1; else return cur+x;
}
return cur;
}
void write(i128 x) {
if(x == 0) return std::cout << "0", void(0);
int stack[128], top = 0;
while(x) stack[top++] = x % 10, x /= 10;
while(top) std::cout << char(stack[--top] + '0');
return ;
}
int main() {
llsi n, x, y;
std::cin >> n >> x >> y;
i128 ans = 0x7fff'ffff'ffff'ffff;
ans *= 998244353;
std::vector<llsi> a(n);
for(auto &a: a) std::cin >> a;
for(llsi p = 0; p <= 30; ++p) {
i128 sum = 0;
for(auto a: a) sum += calc(a, p);
ans = std::min(ans, i128(x) * sum + i128(y) * p);
}
write(ans); std::cout << char(10);
return 0;
}
Postscript
下周可能会打打多校,但博客不一定有空写