2022“杭电杯”中国大学生算法设计超级联赛(1)
1001 | 1002 | 1003 | 1004 | 1005 | 1006 | 1007 | 1008 | 1009 | 1010 | 1011 | 1012 | |
赛时过题 | O | O | O | O | O | |||||||
赛后补题 | 待补 | O | 待补 |
赛后感悟:
可惜了,A只允许线性做法,但是我只会后缀数组,结果就TLE了。。
DC3,后缀自动机SAM,拓展KMP都能过这题但我都不会QAQ
另外这一把宇彬和丁老师发挥的很稳定,所有题目几乎都没怎么卡且一发A,厉害的
说起来D和H是两道写起来很简单的题,只是思路上比A麻烦一点,不想像A几乎是个板子题。
但是DC3的板子我不会啊,SAM也忘光了。。所以考场上知道自己写的代码大概率卡不进时限,就应该看看其他题!
A题112人过,但D题和H题也有50人过,这两题对当时的我来说是更优解!
比赛前期一定要多开题,所有题目都要看!
赛事排名情况:
8题末尾:42名
7题末尾:66名
6题末尾:106名
1011 Random
题目难度:check-in
题目大意:N个[0,1]的随机数,M次操作每次50%概率删掉最大值,50%概率删掉最小值。求剩下的N-M个随机数的和的期望值。
题目解析:设和为随机变量S,每个数对和的贡献为随机变量Xi,则S=X1+...+XN
注意到第i个数ai能对答案做出贡献的前提是取最小值的操作<=i-1&&取最大值的操作=m-最小值操作<=n-i 即 m-n+i<=最小值操作<=i-1
注意到P(最小值操作=x)=C(m,x)*(50%)m
则E(Xi)=ai*P(max(m-n+i,0)<=最小值操作<=i-1)=ai*(50%)m*∑[ C(m,max(m-n+i,0))+...+C(m,i-1) ]
但实际上由于每个数是[0,1]的随机数,期望为1/2,又最终一定剩下n-m个数,则最终的答案就是(n-m)/2,没必要推那么多式子!
参考代码:
查看代码
/*#if(__cplusplus == 201103L)
#include <unordered_map>
#include <unordered_set>
#else
#include <tr1/unordered_map>
#include <tr1/unordered_set>
namespace std
{
using std::tr1::unordered_map;
using std::tr1::unordered_set;
}
#endif*/
#include<bits/stdc++.h>
using namespace std;
long long T,n,m,mod=1e9+7;
long long qmi(long long a, long long k) {
long long res = 1;
while (k) {
if (k & 1)res = res * a%mod;
a = a * a%mod;
k >>= 1;
}
return res;
}
long long re(long long x)
{
return (qmi(x,mod-2))%mod;
}
long long MOD(long long x,long long y)
{
return ((x%mod)*re(y))%mod;
}
int main()
{
cin>>T;
while(T--)
{
cin>>n>>m;
n-=m;
printf("%lld\n",MOD(n,2));
}
return 0;
}
1002 Dragon slayer
题目难度:check-in
题目大意:有一个长度为n,宽为m的长方形,左下角为(0,0),右下角为(n,m)。
起点为(xs+0.5,ys+0.5),终点为(xt+0.5,yt+0.5),另外在图中有k堵墙,给定每堵墙的起点&终点。
问最少拆几堵墙能从起点走到终点?T组数据。
数据范围:T<=10,1<=n,m,k<=15
题目解析:
最多15堵墙,2k枚举每个墙拆/不拆,然后广搜/深搜即可。
需要注意墙是一条线而非若干墙体块,只有两堵墙中有缝隙即可穿过去,因此要把坐标*2,把两堵墙中间的那部分表示成一个整数坐标。
参考代码:
查看代码
#include<bits/stdc++.h>
using namespace std;
int n,m,k,T,sx,sy,tx,ty,max1,ans,dx[10]={0,0,0,-1,1},dy[10]={0,1,-1,0,0};
bool flag[40],line[40][40],vis[40][30];
struct tu{
int ax,ay,bx,by;
}e[40];
void dfs(int x,int y)
{
vis[x][y]=1;
for(int i=1;i<=4;i++)
{
int u=x+dx[i],v=y+dy[i];
if(u<0||v<0||u>n||v>m) continue;
if(vis[u][v]||line[u][v]) continue;
dfs(u,v);
}
}
int main()
{
cin>>T;
while(T--)
{
ans=0;
scanf("%d%d%d",&n,&m,&k);
n*=2;
m*=2;
scanf("%d%d%d%d",&sx,&sy,&tx,&ty);
sx=sx*2+1;
sy=sy*2+1;
tx=tx*2+1;
ty=ty*2+1;
for(int i=1;i<=k;i++)
{
scanf("%d%d%d%d",&e[i].ax,&e[i].ay,&e[i].bx,&e[i].by);
e[i].ax*=2;
e[i].bx*=2;
e[i].ay*=2;
e[i].by*=2;
}
max1=(1<<k)-1;
for(int i=0;i<=max1;i++)
{
int now=0;
memset(flag,0,sizeof(flag));
for(int j=1;j<=k;j++)
{
if((i&(1<<(j-1))))
{
now++;
flag[j]=1;
}
}
if(now<ans)
continue;
memset(line,0,sizeof(line));
memset(vis,0,sizeof(vis));
for(int j=1;j<=k;j++)
{
if(flag[j])
{
for(int l=min(e[j].ax,e[j].bx);l<=max(e[j].ax,e[j].bx);l++)
{
for(int p=min(e[j].ay,e[j].by);p<=max(e[j].ay,e[j].by);p++)
{
line[l][p]=1;
}
}
}
}
dfs(sx,sy);
if(vis[tx][ty])
ans=now;
}
printf("%d\n",k-ans);
}
return 0;
}
/*
2
3 2 2
0 0 2 1
0 1 3 1
1 0 1 2
3 2 2
0 0 2 1
1 0 1 1
2 1 2 2
*/
1003 Backpack
题目难度:easy
题目大意:n个物品,每个物品有体积vi和价值wi,背包容量为m,求刚好填满背包的最大价值异或和。T组数据。
数据范围:1<=T<=10,1<=n,m,vi,wi<210
题目解析:
注意到如果普通二维背包无法求最大异或和,必须使用三维背包,dp[i][j][k]表示前i个物品,物品总容量为j,价值异或和能否达到k。时间复杂度为O(T*n*m*vmax)约等于1010
做个转化,以物品价值为下标进行背包,dp[i][j][k]表示前i个物品,价值异或和为j,能否填充容量k,这样就可以使用bitset优化了。时间复杂度为O(T*n*m*vmax/32)约等于3*108
赛后总结:做dp时不妨把状态转移式想清楚、写好,然后再动手写程序,会减少对电脑的占用时间和debug的时间。
一开始没写滚动数组,后来发现第一维不可省略,因为j^w可能>j,改成滚动数组花了一定不少时间;
另外把判断条件dp[n&1][i][m]写成dp[n&1][i][n],把dp[(i&1)^1][j^w]<<v写成dp[(i&1)^1][j-w]<<v也花了不少时间debug...
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#include<bitset>
#define For(i,a,b) for(int i=a;i<=b;i++)
#define Frd(i,a,b) for(int i=a;i>=b;i--)
const int N=1100;
std::bitset<N> dp[2][N];
int main()
{
int T;scanf("%d",&T);
while (T--)
{
int n,m;scanf("%d%d",&n,&m);
Frd(i,1024-1,0) dp[0][i]=0,dp[1][i]=0;dp[0][0][0]=1;
For(i,1,n)
{
int v,w;scanf("%d%d",&v,&w);
Frd(j,1024-1,0) dp[i&1][j]=dp[(i&1)^1][j]|(dp[(i&1)^1][j^w]<<v);
}
int flag=0;
Frd(i,1024-1,0) if (dp[n&1][i][m])
{
printf("%d\n",i);
flag=1;
break;
}
if (!flag) printf("%d\n",-1);
}
return 0;
}
1012 Alice and Bob
题目难度:easy-medium
题目大意:Alice和Bob在玩游戏,黑板上有m个[0,N]的整数,Alice将整数分成两组(一个组可为空),Bob选择一组留下并将这组内的所有数-1。
如果某个时刻黑板上有0,则Alice赢。如果黑板上没有数字了就是Bob赢。给定m个整数,问最后谁能赢。
数据范围:T<=2000,1<=∑N<=1e6
题目解析:将题意中的-1转化为*2
将每个值为i 的数字视为2n-i,那么Alice的胜利条件就是最终局面中能出现 2n。
Alice将数字分成两个集合,Bob将其中一个集合-1,就相当于将这个集合中的数全部乘2,然后将另一个集合删去。
如果Alice能将集合中的数字按照值二等分,那么无论Bob怎么操作,黑板上所有数字的总和实际是不变的。
如果集合中的数字总和超过2n ,由于所有数字都是不超过2n 的 2的幂次,那么Alice的每次分割总能使得两边集合的值均≥2n-1。
因此直接判断所有数字的2的幂次的总和即可。
赛时经历:
一开始丁老师提出Alice可以将所有数的数量平分,然后再分成两个集合,因此必须有一个数x的个数>=2x
我提出像1 2 2这种情况1的个数<2,2的个数<4,但是Alice可以把它分成1和 2 2,Bob必须得删1否则立刻就输了,但是2 2会变成1 1,然后Bob无论删哪一个1都必输。
然后我又从终态往前推,发现把两个任意子必胜序列并起来再让所有数+1,还是一个必胜序列(Alice可以把新必胜序列分成两个同样的子必胜序列,Bob无论选哪个-1都必败):
必胜序列\各数字个数 | 0 | 1 | 2 | 3 | 4 | 5 |
A | 1个 | |||||
B=AUA | 0个 | 2个 | ||||
C=AUB | 0个 | 1个 | 2个 | |||
D=BUB | 0个 | 0个 | 4个 | |||
E=AUC | 0个 | 1个 | 1个 | 2个 | ||
F=BUC | 0个 | 0个 | 3个 | 2个 | ||
G=CUC | 0个 | 0个 | 2个 | 4个 |
最后可以发现,任意一个数i等价于两个i+1(两个i+1一定可以分到2个集合中,然后其中一个-1变成i),那么相对的2个i+1等价于1个i。
(其实赛时并没有证出这个性质的正确性,不过直观上感觉是对的)
从后往前递推,如果0的个数>1那么就是Alice赢。
参考代码:
查看代码
#include<bits/stdc++.h>
#define _CRT_SECURE_NO_WARNINGS
#define int long long
using namespace std;
const int N = 1e6 + 10;
int a[N];
signed main() {
int T;
scanf("%lld", &T);
while (T--) {
int n;
scanf("%lld", &n);
for (int i = 0; i < n + 1; i++)scanf("%lld", &a[i]);
int res = 0;
for (int i = n ; i >= 0; i--)res = a[i] + res / 2;
if (res > 0)puts("Alice");
else puts("Bob");
}
}
1009 Laser
题目难度:easy-medium
题目大意:平面上有N个点,问是否能在平面上找到一个点P使这N个点都能被P点发出的米字型射线穿透。
数据范围:T<=1e5,∑N<=5e5,-1e8<=x,y<=1e8
题目解析:
由于每个点都一定要被米字型射线穿透,可以随便取一个点,枚举它是被哪种射线(横线、竖线、对角线、反对角线)穿过。
然后再取一个没被该射线穿透的点,枚举这个点是被哪种射线穿过。
根据两条射线得到中心点P,再判断是否所有点可以被米字型射线穿透。
时间复杂度O(N*16)
赛时经历:我一开始还没反应过来怎么做,以为必须要先枚举横线再枚举竖线来确定中心点。
这道题的关键性质是 每个点都一定要被米字型射线的某种射线穿透,这样只要枚举不共线的两点分别被什么射线穿透即可。
还是宇彬反应快,TQL。而且丁老师的码力也有进步,挺长的代码一遍AC。
参考代码:
查看代码
#include<bits/stdc++.h>
#define _CRT_SECURE_NO_WARNINGS
#define int long long
using namespace std;
#define x first
#define y second
const int N = 5e5+10;
typedef pair<int, int> PII;
int n;
PII p[N];
bool check(int cx, int cy, int x, int y) {
if (cx == x || cy == y || cx - x == cy - y || cx - x == y - cy)return 1;
else return 0;
}
bool work(int *tm) {
if (tm[0] == 0) {
int x = p[1].x;
int i = 1;
while (p[i].x == x && i <= n)i++;
if (i > n) {
return 1;
}
if (tm[1] == 1) {
int y = p[i].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 2) {
int y = x - p[i].x + p[i].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 3) {
int y = -x + p[i].x + p[i].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
}
if (tm[0] == 1) {
int y = p[1].y;
int i = 1;
while (p[i].y == y && i <= n)i++;
if (i > n) {
return 1;
}
if (tm[1] == 0) {
int x = p[i].x;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 2) {
int x = y + p[i].x - p[i].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 3) {
int x = -y + p[i].y + p[i].x;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
}
if (tm[0] == 2) {
int i = 1;
while (p[i].y-p[1].y==p[i].x-p[1].x && i <= n)i++;
if (i > n) {
return 1;
}
if (tm[1] == 0) {
int x = p[i].x;
int y = x - p[1].x + p[1].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 1) {
int y = p[i].y;
int x = y + p[1].x - p[1].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 3) {
int y = (-p[1].x + p[1].y + p[i].x + p[i].y) / 2;
int x = y+p[1].x-p[1].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
}
if (tm[0] == 3) {
int i = 1;
while (p[i].y - p[1].y == -(p[i].x - p[1].x) && i <= n)i++;
if (i > n) {
return 1;
}
if (tm[1] == 0) {
int x = p[i].x;
int y = -(x - p[1].x) + p[1].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 1) {
int y = p[i].y;
int x = -y + p[1].x + p[1].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
if (tm[1] == 2) {
int y = (-p[i].x + p[i].y + p[1].x + p[1].y) / 2;
int x = y + p[i].x - p[i].y;
for (int i = 1; i <= n; i++) {
if (!check(p[i].x, p[i].y, x, y))return 0;
}
return 1;
}
}
}
signed main() {
int T;
scanf("%lld", &T);
while (T--) {
scanf("%lld", &n);
for (int i = 1; i <= n; i++) {
scanf("%lld%lld", &p[i].x, &p[i].y);
p[i].x <<= 1, p[i].y <<= 1;
}
int tm[4] = { 0,1,2,3 };
bool f = 0;
do {
if (work(tm)) {
puts("YES");
f = 1;
break;
}
} while (next_permutation(tm, tm + 4));
if (!f) {
puts("NO");
}
}
}
1004 Ball
题目难度:medium
题目大意:M*M平面上有N个点,定义P(x1,y1)和Q(x2,y2)的距离为|x1-x2|+|y1-y2|。
求问有多少无序三元组(A,B,C)满足:AB,AC,BC3条边权值中间的那个边的权值为质数。
数据范围:1<=N<=2000,1<=x,y<=M<=1e5
题目解析:直接枚举三个点复杂度太高。
考虑枚举中间为质数的那条边(两端为A,B),然后求有多少个点C满足dis(AC)<=dis(AB)&&dis(AB)<=dis(BC) || dis(BC)<=dis(AB)&&dis(AB)<=dis(AC)。
需要注意即使选择相同三个点,选择不同边为中间为质数的边对应的是相同方案(比如 3 3 4实际上只会被第二次出现的权值为3的边算1次,第一次的权值为3的边不会算入)。
可以使用bitset优化,从小到大枚举当前边AB,当前边之前的边都是<=当前边的,之后的边都是>=当前边的,考虑多少个点C可以通过小边连到A(B),大边连到B(A)
(同一个三元组一定只会被算一次,这个三元组对应的三条边一个在当前边前面,一个在当前边后面)
(第三条边不会和当前边相同,如果第三条边和当前边相同,那么也就是当前边之前所枚举的边中 有一条边的两个端点是相同节点,但实际上没有)
另外需要注意在时限很紧的情况下vector的耗时会比提前分配结构体空间多不少,最好还是使用结构体数组而非vector。
参考代码:
查看代码
#include<iostream>
#include<cstdio>
#include<vector>
#include<algorithm>
#include<bitset>
#define For(i,a,b) for(int i=a;i<=b;i++)
#define ABS(x) ((x)<0?-(x):(x))
const int N=2010;
std::pair<int,int> p[N];
struct Edge
{
int v,i,j;
}e[N*N];int e_cnt;
//std::vector<Edge> e;
int cmp(const Edge&a,const Edge&b)
{
return a.v<b.v;
};
std::bitset<N> tmp[N];
const int M=4e5+1000;
int flag[M],prime[M],cnt;
void pre(int n)
{
flag[1]=1;//这个要注意
For(i,2,n)
{
if (!flag[i]) prime[++cnt]=i;
For(j,1,cnt)
{
if (i*prime[j]>n) break;
flag[i*prime[j]]=1;
if (i%prime[j]==0) break;
}
}
}
int main()
{
freopen("1004.in","r",stdin);
int T;scanf("%d",&T);pre(4e5);
while (T--)
{
int n,m;scanf("%d%d",&n,&m);
For(i,1,n) scanf("%d%d",&p[i].first,&p[i].second);
// e.clear();
e_cnt=0;
// For(i,1,n) For(j,1,i-1) e.push_back((Edge){ABS(p[i].first-p[j].first)+ABS(p[i].second-p[j].second),i,j});
For(i,1,n) For(j,1,i-1) e[++e_cnt]=(Edge){ABS(p[i].first-p[j].first)+ABS(p[i].second-p[j].second),i,j};
// std::sort(e.begin(),e.end(),cmp);//排序所有边
std::sort(e+1,e+1+e_cnt,cmp);
long long ans=0;For(i,1,n) tmp[i].reset();
// For(i,0,(int)e.size()-1)
For(i,1,e_cnt)
{
if (!flag[e[i].v]) ans+=(tmp[e[i].i]^tmp[e[i].j]).count();
//tmp[e[i].i]为 e[i].i通过比e[i].v短的边能到的点
//~tme[e[i].j]为e[i].j通过比e[i].v长的边能到的点
//(A&~B)|(~A&B)=A^B
tmp[e[i].i][e[i].j]=1;
tmp[e[i].j][e[i].i]=1;
}
printf("%lld\n",ans);
}
return 0;
}
1001 String
题目难度:medium
题目大意:对一个字符串G定义函数FG为满足下列条件的x的个数:
1、1<=x<=Glen
2、G的前x位字符串G[1..x] 和 G的后x位字符串[Glen-x+1,Glen] 完全相同
3、G[1..x]和G[Glen-x+1,Glen]的区间交集长度得是k的倍数且不可为0(即k,2k,3k,....)
求出(FS[1..1]+1)*(FS[1..2]+1)*...*(FS[1..Slen]+1) mod 998244353
数据范围:
T<=10,Slen<=1e6
题目解析:
使用exkmp或SAM、DC3等线性做法求出1、2条件满足的x
使用模意义差分满足条件3
赛时经历:
比赛时只会后缀数组,但这个是O(nlogn)的做法,好不容易写完而且感觉很对却没能卡过时限。。。
之后在板子里发现了DC3,但由于完全没学过DC3,连DC3的函数怎么用的不会,而且不知道为啥板子里把字符串str的传参设为了int[]。。。
果然别人的板子还是不靠谱,还得是自己的板子才靠谱,害。。
参考代码:(待补)
1008 Path
题目难度:medium
题目大意:N个点M条边的的有向图,每个边有权值w。
每条边可能是普通边或特殊边,走过特殊边以后下一步可以若走某条边i(普通边/特殊边)则开销为wi-K,如果不走某条边(即去无法通过某条边到达的点)则开销为0。
询问起到到所有其他点的最小开销。T组数据。
数据范围:1<=∑n,∑m<=1e6
题目解析:拆点,把每个点拆成2个点,分别对应上一步是普通边/特殊边,然后就是dijs。
由于点的数量级是1e6,对于上一步是特殊边的那个点来说不能暴力对所有没连边的点建边,这样的话边数会退化成n2。
由于这类边的开销为0,根据dijs算法的性质每次取出的都是当前总开销最小的点,如果这个点对应的上一步是特殊边,那么所有它非直连的未确定最小开销的点都可以直接确定最小开销了。
也就是说在dijs的时候维护一个set,里面存所有未确定最小开销的上一步是普通边的点。
如果当前点对应的上一步是特殊边,那就把set中的所有点全部枚举一遍,如果不与当前点直连那么直接更新即可。
在这个过程中,对于set中的每个点,它最多被从set取出来的次数为能直连到它的边数+1(最后一次取出来以后直接更新最小开销然后删了)。因此总复杂度为O((m+n)logn)
再加上dijs的复杂度是O(nlogm),总复杂度是O((m+n)log)
参考代码:(待补)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具