隐藏页面特效

2024牛客暑期多校训练营2

1|0Preface


最下班的一集,100min 的时候手速过了六题,本来以为是完美开场,没想到是坐牢的开始

J 题很快推出了一个 O(n) 计算的组合式子,然后扔给徐神找生成函数做法,中间给出了几个要写快速阶乘算法的假做法后发现这题不太可做

祁神开始转战 D 题后给了个基于纳什均衡的很对的 DP 做法,然后二分的转移写了半天过不了样例,最后换成枚举后正确性是有保证了,但交上去又 TLE 了

过的人比较多的 G 题因为基本没怎么话时间想因此没意识到是个丁真题,最后徐神发现了 1000 以内的质数只有 11 个,但已经于事无补了

最后靠着前期优秀的罚时苟了个尚可的排名,但后 200min 不出题让人感觉梦回去年啊


2|0Floor Tiles


徐神看了眼就秒了,我题意都不知道,纯纯的被带飞

#include <bits/stdc++.h> int main() { std::ios::sync_with_stdio(false); int T; std::cin >> T; while(T--) { int n, m, k; std::cin >> n >> m >> k; int x, y; std::string t; std::cin >> x >> y >> t; x--; y--; if(k < n + m) { std::cout << "No\n"; continue; } std::vector<std::string> ans(n, std::string(m, 'A')); if((x + y & 1) ^ (t[0] == 'A')) for(int i = 0; i < n; ++i) for(int j = 0; j < m; ++j) ans[i][j] = char('A' + (i + j & 1)); else for(int i = 0; i < n; ++i) for(int j = 0; j < m; ++j) ans[i][j] = char('B' - (i + j & 1)); auto is_circle = [&](int i, int j) -> bool { return ans[i][j] == 'A' && ans[i][j + 1] == 'B' && ans[i + 1][j] == 'B' && ans[i + 1][j + 1] == 'A'; }; int cir = 0, type = 0; for(int i = 0; i < n - 1; ++i) for(int j = 0; j < m - 1; ++j) if(is_circle(i, j)) { cir += 1; if(i == x && j == y) type = 1; } if(n + m + cir < k) { std::cout << "No\n"; continue; } for(int i = 0; i < n - 1 && n + m + cir > k; ++i) for(int j = 0; j < m - 1 && n + m + cir > k; ++j) { if(is_circle(i, j)) { if(type) ans[i][j + 1] = 'A'; else ans[i][j] = 'B'; cir -= 1; } } std::cout << "Yes" << char(10); for(int i = 0; i < n; ++i) std::cout << ans[i] << char(10); } return 0; }

3|0MST


感觉数据水了赛时随便写了个带 log 的东西交上去一发过了,后面发现是有严格根号的做法的说

这种询问集合的题很容易想到根号分治,显然当 ki 较大时直接 O(mα(m)) 跑一个克鲁斯卡尔即可

ki 较小时理论上我们需要严格的 O(ki2) 做法,很容易想到用 Prim,但问题在于我们无法快速找出这些点之间的边

一种解决方案是对边的存储也使用根号分治,将点按照度数分类

度数 >m 的点数较少,可以用临界矩阵存储;否则剩下的度数 m 的点对应的边数较少,可以用邻接表存储

当然比赛时我直接用 map 存两点之间的边,然后还是暴力跑克鲁斯卡尔,交上去也过了

#include<cstdio> #include<iostream> #include<map> #include<vector> #include<algorithm> #define RI int #define CI const int& using namespace std; typedef pair <int,int> pi; const int N=100005,LIM=250; struct edge { int x,y,v; inline edge(CI X=0,CI Y=0,CI V=0) { x=X; y=Y; v=V; } friend inline bool operator < (const edge& A,const edge& B) { return A.v<B.v; } }E[N]; int n,m,q,k,p[N],fa[N]; map <pi,int> rst; inline int getfa(CI x) { return fa[x]!=x?fa[x]=getfa(fa[x]):x; } namespace Case1 { inline void solve(void) { RI i,j; for (i=1;i<=k;++i) fa[p[i]]=p[i]; vector <edge> edges; for (i=1;i<=k;++i) for (j=i+1;j<=k;++j) { int x=p[i],y=p[j]; if (x>y) swap(x,y); if (rst.count(pi(x,y))) edges.emplace_back(x,y,rst[pi(x,y)]); } sort(edges.begin(),edges.end()); int cnt=0; long long ret=0; for (auto [x,y,v]:edges) { if (getfa(x)!=getfa(y)) { fa[getfa(x)]=getfa(y); ++cnt; ret+=v; } if (cnt==k-1) break; } if (cnt!=k-1) puts("-1"); else printf("%lld\n",ret); } }; namespace Case2 { bool key[N]; inline void solve(void) { RI i; for (i=1;i<=k;++i) fa[p[i]]=p[i],key[p[i]]=1; int cnt=0; long long ret=0; for (i=1;i<=m;++i) { int x=E[i].x,y=E[i].y; if (!key[x]||!key[y]) continue; if (getfa(x)!=getfa(y)) { fa[getfa(x)]=getfa(y); ++cnt; ret+=E[i].v; } if (cnt==k-1) break; } for (i=1;i<=k;++i) key[p[i]]=0; if (cnt!=k-1) puts("-1"); else printf("%lld\n",ret); } } int main() { RI i; for (scanf("%d%d%d",&n,&m,&q),i=1;i<=m;++i) { scanf("%d%d%d",&E[i].x,&E[i].y,&E[i].v); if (E[i].x>E[i].y) swap(E[i].x,E[i].y); } for (sort(E+1,E+m+1),i=1;i<=m;++i) if (!rst.count(pi(E[i].x,E[i].y))) rst[pi(E[i].x,E[i].y)]=E[i].v; while (q--) { for (scanf("%d",&k),i=1;i<=k;++i) scanf("%d",&p[i]); if (k<=LIM) Case1::solve(); else Case2::solve(); } return 0; }

4|0Red Walking on Grid


首先不难发现假设起点在一个连通块的最左边,则存在一种最优的方案使得全程不会往左走

那么在第一行的点只有向下和向右两种操作了;在第二行的点就只能向上或向下

考虑 DP,令 fi,j,0/1 表示当前位于 (i,j) 位置,与其同列的另一个格子是否被经过的最长长度,转移十分显然

#include<cstdio> #include<iostream> #define RI register int #define CI const int& using namespace std; const int N=1e6+5; int n,f[2][N][2]; char a[2][N]; int main() { RI i,j; for (scanf("%d",&n),i=0;i<2;++i) scanf("%s",a[i]+1); for (j=1;j<=n;++j) { for (i=0;i<2;++i) if (a[i][j]=='R') f[i][j][0]=max(f[i][j][0],1); for (i=0;i<2;++i) if (a[i][j]=='R') { if (a[i^1][j]=='R') f[i^1][j][1]=max(f[i^1][j][1],f[i][j][0]+1); } for (i=0;i<2;++i) if (a[i][j]=='R') { if (j+1<=n&&a[i][j+1]=='R') f[i][j+1][0]=max(f[i][j+1][0],max(f[i][j][0],f[i][j][1])+1); } } int ans=0; for (i=0;i<2;++i) for (j=1;j<=n;++j) ans=max(ans,max(f[i][j][0],f[i][j][1])-1); return printf("%d",ans),0; }

5|0Taking Candies


标算感觉十分奇怪,这里讲下祁神赛时写的做法,正确性是有保证的,但赛时 O(n×x2) TLE 了

首先很容易发现两人每次拿糖果都是拿当前最多的一袋,而关注最后一轮博弈过程,显然是谁钱多谁就拿走最后一袋

考虑从后往前递推,不难发现这是个纳什博弈模型,令 fi,x 表示还剩下 i 袋糖果,先手此时手中有 x 块钱,在剩下的过程中最多能得到的糖果数

考虑枚举先手这一轮使用的筹码数 M,若后手选择阻止先手(需要至少 M+1 的钱)会导致后续给先手带来的收益更大,则后手显然不会操作,则有转移:

fi,xfi1,xM+ai (fi1,xM+ai<fi1,x+M+1)

否则分情况讨论一些特殊情况即可,总复杂度 O(n×x2),赛时最后挣扎了一波搞了个记搜剪了点状态,发现还是过不了

#include<bits/stdc++.h> using namespace std; #define int long long const int N = 1e5+5; int n, xx, yy, tot, A[N]; int f[N][205]; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n >> xx >> yy; tot = xx+yy; for (int i=1; i<=n; ++i) cin >> A[i]; sort(A+1, A+n+1); for (int x=(tot+1)/2; x<=tot; ++x) f[1][x]=A[1]; for (int i=2; i<=n; ++i){ for (int x=0; x<=tot; ++x){ int L=0, R=min(x, tot-x); for (int M=L; M<=R; ++M){ if (f[i-1][x-M]+A[i] < f[i-1][x+M+1]){ f[i][x] = max(f[i][x], f[i-1][x-M]+A[i]); }else{ if (M >= tot-x) f[i][x] = max(f[i][x], f[i-1][x-M]+A[i]); else f[i][x] = max(f[i][x], f[i-1][x+M+1]); } } } } cout << f[n][xx] << '\n'; return 0; }

Upt:祁神赛后用了种神秘的两次二分把这题 O(n×xlog(x)) 淦过去了,主要是这个 fi, 它是阶梯上升的,因此要找关键点会比较麻烦

#include<bits/stdc++.h> using namespace std; #define int long long const int N = 1e5+5; const int INF = 1e18+5; int n, xx, yy, tot, A[N]; int f[N][205]; signed main(){ // freopen("1.in", "r", stdin); // freopen("1.out", "w", stdout); ios::sync_with_stdio(0); cin.tie(0); cin >> n >> xx >> yy; tot = xx+yy; for (int i=1; i<=n; ++i) cin >> A[i]; sort(A+1, A+n+1); for (int i=1; i<=n; ++i) f[i][tot+1] = INF; for (int x=(tot+1)/2; x<=tot; ++x) f[1][x]=A[1]; for (int i=2; i<=n; ++i){ for (int x=0; x<=tot; ++x){ int y = tot-x; int bd1=-1, bd2=-1; int L=0, R=min(x, y); while (L<R){ int M = L+(R-L)/2+1; if (f[i-1][x-M]+A[i] > f[i-1][x+M+1]) L=M; else R=M-1; } bd2=L; L=0, R=min(x, y); while (L<R){ int M = L+(R-L)/2; if (f[i-1][x-M]+A[i] < f[i-1][x+M+1]) R=M; else L=M+1; } bd1=L; if (x+bd2+1<=tot) f[i][x] = max(f[i][x], f[i-1][x+bd2+1]); else f[i][x] = max(f[i][x], f[i-1][x-bd2]+A[i]); if (bd1>bd2){ f[i][x] = max(f[i][x], f[i-1][x-bd1]+A[i]); } if (bd1>bd2+1){ f[i][x] = max(f[i][x], min(f[i-1][x+(bd2+1)+1], f[i-1][x-(bd1-1)]+A[i])); } cout << f[n][xx] << '\n'; return 0; }

6|0GCD VS XOR


徐神开场写的,还抢到了二血,而我题意都不知道

#include <bits/stdc++.h> int main() { std::ios::sync_with_stdio(false); int T; std::cin >> T; while(T--) { int64_t x; std::cin >> x; int64_t l = x&-x; if(l == x) std::cout << "-1\n"; else std::cout << (x ^ l) << char(10); } return 0; }

7|0Mixed Juice


这场的防 AK,直接弃疗


8|0The Set of Squares


唐完了,早知道就不该把 J 题扔给徐神,不然徐神早一小时看这题可能都出了

需要发现 <1000=32 的质数只有 11 个,这也就意味着每个数最多含有一个 32 的质数

因此这些较大的质数只能相互匹配,所以可以分开来处理

而较小的质数由于数量很小,并且我们只关心每个质数此时的次数是奇数还是偶数,因此可以状压

用一个类似背包的 DP 即可做到 O(211×n),转移十分显然

#include<cstdio> #include<iostream> #include<vector> #include<utility> #define RI register int #define CI const int& using namespace std; typedef pair <int,int> pi; const int N=1<<11,pri[11]={2,3,5,7,11,13,17,19,23,29,31},mod=1e9+7; int n,x,f[2][N][2]; vector <pi> vec[N]; inline void inc(int& x,CI y) { if ((x+=y)>=mod) x-=mod; } int main() { RI i,j; for (scanf("%d",&n),i=1;i<=n;++i) { scanf("%d",&x); int mask=0,val=1; for (j=0;j<11;++j) { while (x%pri[j]==0) { if ((mask>>j)&1) val=1LL*val*pri[j]%mod; x/=pri[j]; mask^=(1<<j); } } vec[x].push_back(pi(mask,val)); } int now=0; f[0][0][0]=1; auto calc=[&](CI x,CI y) { int val=1; for (RI i=0;i<11;++i) if (((x&y)>>i)&1) val=1LL*val*pri[i]%mod; return val; }; for (auto [mask,val]:vec[1]) { int nxt=now^1; for (i=0;i<N;++i) f[nxt][i][0]=f[now][i][0]; for (i=0;i<N;++i) inc(f[nxt][i^mask][0],1LL*f[now][i][0]*val%mod*calc(mask,i)%mod); now=nxt; } for (j=32;j<=1000;++j) if (!vec[j].empty()) { for (i=0;i<N;++i) f[now][i][1]=0; for (auto [mask,val]:vec[j]) { int nxt=now^1; for (i=0;i<N;++i) f[nxt][i][0]=f[now][i][0],f[nxt][i][1]=f[now][i][1]; for (i=0;i<N;++i) { inc(f[nxt][i^mask][0],1LL*f[now][i][1]*val%mod*calc(mask,i)%mod*j%mod); inc(f[nxt][i^mask][1],1LL*f[now][i][0]*val%mod*calc(mask,i)%mod); } now=nxt; } } return printf("%d",(f[now][0][0]-1+mod)%mod),0; }

9|0Instructions Substring


不难发现枚举左端点 l,假设其对应的第一个走到 (x,y) 的位置为 r,则贡献为 nr+1

判断是否走到的充要条件是 XrXl1=xYrYl1=y,其中 X,Y 分别为两维坐标的前缀和数组

倒着用 map 维护一下最靠左的 (Xi,Yi) 即可

#include<cstdio> #include<iostream> #include<map> #include<utility> #define RI register int #define CI const int& using namespace std; typedef pair <int,int> pi; const int N=200005; int n,x,y,px[N],py[N]; map <pi,int> fst; char a[N]; int main() { RI i; scanf("%d%d%d%s",&n,&x,&y,a+1); if (x==0&&y==0) return printf("%lld",1LL*n*(n+1)/2LL),0; for (i=1;i<=n;++i) { px[i]=px[i-1]; py[i]=py[i-1]; if (a[i]=='A') --px[i]; else if (a[i]=='D') ++px[i]; else if (a[i]=='W') ++py[i]; else if (a[i]=='S') --py[i]; } //for (i=1;i<=n;++i) printf("%d %d\n",px[i],py[i]); long long ans=0; for (i=n;i>=1;--i) { fst[pi(px[i],py[i])]=i; pi tmp=pi(x+px[i-1],y+py[i-1]); if (fst.count(tmp)) ans+=n-fst[tmp]+1; } return printf("%lld",ans),0; }

10|0Red Playing Cards


看完题很容易想到一个暴力的 O(n3) 区间 DP 做法,但题面显然要求的是 O(n2)

考虑对于一个区间,在删除它之前可以先对其内部的一些较小的区间进行操作;而不在其内部的区间要么和它没有影响,要么会导致该区间无法操作

因此把所有区间按照长度从小到大处理,每次枚举其内部从哪些区间进行了操作,总复杂度 O(n2)

#include<bits/stdc++.h> using namespace std; const int N = 6005; int n; int A[N], pos[N][2], len[N], id[N]; int f[N][N]; signed main(){ ios::sync_with_stdio(0); cin.tie(0); cin >> n; pos[0][0]=0, pos[0][1]=2*n+1, len[0]=2*n+1; for (int i=1; i<=2*n; ++i){ cin >> A[i]; if (pos[A[i]][0]==0) pos[A[i]][0]=i; else pos[A[i]][1]=i, len[A[i]]=pos[A[i]][1]-pos[A[i]][0]; } for (int i=0; i<=n; ++i) id[i]=i; sort(id, id+n+1, [&](int a, int b){return len[a]<len[b];}); for (int dd=0; dd<=n; ++dd){ int x=id[dd]; int L=pos[x][0], R=pos[x][1]; f[x][L]=x; for (int i=L+1; i<=R; ++i){ f[x][i] = f[x][i-1]+x; if (pos[A[i]][1]==i && pos[A[i]][0]>=L){ f[x][i] = max(f[x][i], f[x][pos[A[i]][0]-1] + f[A[i]][pos[A[i]][1]]); } } // printf("dd=%d f[%d]=%d\n", dd, x, f[x][R]); } // for (int i=0; i<=n*2+1; ++i) printf("%d ", f[0][i]); puts(""); cout << f[0][2*n+1] << '\n'; return 0; }

11|0Involutions


只会这题的组合意义式子,但 O(n) 的做法还是过不了,生成函数也没办法处理

考虑枚举不动点的个数 i,则剩下的 2y=ni 个点构成的置换环长度都必须恰好为 2

C2yy 表示选出 y 个点作为起点,y! 表示剩下的 y 个点关于这些点的匹配方案,但由于两边是对称的,因此要除以 2y

最后的式子就是:

y=02ynan2y×C2yy×y!2y

然而因为没推递推的关系式,所以根本想不到正解是整式递推(知道了也没用反正写不来),直接弃疗


12|0Postscript


白天打牛客多校,晚上还有 CF 1+2,明天还有杭电多校,真是冲冲又实实啊


__EOF__

本文作者hl666
本文链接https://www.cnblogs.com/cjjsb/p/18310413.html
关于博主:复活的ACM新生,目前爱好仅剩Gal/HBR/雀魂/单机/OSU
版权声明:转载请注明出处
声援博主:欢迎加QQ:2649020702来DD我
posted @   空気力学の詩  阅读(413)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
历史上的今天:
2023-07-18 UESTC 2023 Summer Training #08 Div.2
2020-07-18 Luogu P2150 [NOI2015]寿司晚宴
点击右上角即可分享
微信分享提示