2021年中国大学生程序设计竞赛女生专场
A. 公交线路
检查坐对方向和坐反方向两种情况对应的报站序列是否符合输入,如果都符合那就是''Unsure'',否则可以确定有没有坐反。
#include<cstdio> const int N=15; int n,m,S,T,i,A,B,a[N],b[N]; bool check(int d){ int i,j; for(i=S+d,j=1;j<=m;j++,i+=d){ if(i<1||i>n)return 0; if(a[i]!=b[j])return 0; } return 1; } int main(){ scanf("%d%d%d",&n,&S,&T); for(i=1;i<=n;i++)scanf("%d",&a[i]); scanf("%d",&m); for(i=1;i<=m;i++)scanf("%d",&b[i]); A=check(S<T?1:-1); B=check(S<T?-1:1); if(A&&B)puts("Unsure"); else if(A)puts("Right"); else puts("Wrong"); }
B. 攻防演练
考虑如何判断一个字符串 $t = t_1 t_2 \ldots t_k$ 是否是 $s_{l,r} = s_l s_{l+1} \ldots s_r$ 的子序列:用一个指针从 $l$ 开始往右进行扫描,找到第一个 $t_1$ 并停下来,然后从那个位置接着往右找到第一个 $t_2$,$\dots$ 如果按这个贪心方式能找完所有 $k$ 个字符,那么 $t$ 就是 $s_{l,r}$ 的子序列;如果指针扫到了 $r+1$ 甚至更远的地方,那么说明 $t$ 不是 $s_{l,r}$ 的子序列。
现在考虑如何寻找一个最短的 $t$,使得 $t$ 不是 $s_{l,r}$ 的子序列。假设指针目前位于 $x$,$t$ 的下一个字符有 $m$ 种选项,选第 $i$ 种选项时,指针将扫到 $x$ 右边第一个字符 $i$ 的位置 $v_{x,i}$(如果不存在 $i$ 那么 $x$ 将扫到 $n + 1$)。那么为了使得指针往右扫描得尽量远,$t$ 的下一个字符应该选择 $v_{x,i}$ 最大的那个 $i$。因此,预处理出 $nxt_x$ 表示指针位于 $x$ 时,下一步将扫到哪个位置,无论询问的 $[l,r]$ 是什么,它都是一个定值,不随 $[l,r]$ 而改变。
于是问题转化为:从 $l$ 开始沿着 $nxt$ 一路往右跳,要跳多少步才能跳到 $>r$ 的地方?这是个经典问题,可以使用倍增;也可以离线询问后,将 $nxt_x$ 当作 $x$ 的父亲建出一棵有根树,在树上进行二分查找。
时间复杂度 $O(nm+q\log n)$。
#include<bits/stdc++.h> using namespace std; const int MAXN=200005; const int MAXD=18; char str[MAXN]; int go[MAXD][MAXN]; int main() { int n,m,q; scanf("%d%d%s%d",&m,&n,str+1,&q); vector<int> nxt(m,n+1); go[0][n+1]=n+1; for(int i=n;i>=0;i--) { for(int j=0;j<m;j++) go[0][i]=max(go[0][i],nxt[j]); if(i)nxt[str[i]-'a']=i; } for(int i=1;i<MAXD;i++) for(int j=0;j<=n+1;j++) go[i][j]=go[i-1][go[i-1][j]]; for(int i=0;i<q;i++) { int l,r; scanf("%d%d",&l,&r); int t=l-1,res=0; for(int d=MAXD-1;d>=0;d--) if(go[d][t]<=r)t=go[d][t],res+=(1<<d); printf("%d\n",res+1); } return 0; }
C. 连锁商店
如果某家公司开的连锁店数量不超过 $1$,那么可以无视``每家公司的红包只能领一份''这个限制,这是因为任何一条路线都无法访问多次该公司开的商店。如果某家公司开的连锁店数量至少为 $2$,那么这样的公司数最多为 $\frac{n}{2}\leq 18$。
由于第二类公司数量并不多,因此可以使用状态压缩动态规划来求解这个问题。设 $f[i][S]$ 表示从 $1$ 出发到达了 $i$ 点,一路上访问过的第二类公司集合为 $S$ 时,访问过的第一类公司的红包总价值最大是多少,枚举下一个景点进行转移。
时间复杂度 $O(n^22^{\frac{n}{2}})$。
#include<cstdio> const int N=40,M=18; int n,m,cnt,i,j,tmp,x,y,col[N],ap[N],w[N],id[N],g[N][N],f[N][(1<<M)+1],s[(1<<M)+1]; inline void up(int&a,int b){if(a<b)a=b;} int main(){ scanf("%d%d",&n,&m); for(i=1;i<=n;i++)scanf("%d",&col[i]),ap[col[i]]++; for(i=1;i<=n;i++){ scanf("%d",&w[i]); if(ap[i]>=2)id[i]=cnt++;else id[i]=-1; } while(m--)scanf("%d%d",&x,&y),g[x][y]=1; for(i=1;i<=n;i++)for(j=0;j<1<<cnt;j++)f[i][j]=-1; if(~id[col[1]])f[1][1<<id[col[1]]]=0;else f[1][0]=w[col[1]]; for(i=1;i<n;i++)for(j=0;j<1<<cnt;j++)if(~f[i][j]){ tmp=f[i][j]; for(x=i+1;x<=n;x++)if(g[i][x]){ if(~id[col[x]])up(f[x][j|(1<<id[col[x]])],tmp); else up(f[x][j],tmp+w[col[x]]); } } for(i=1;i<1<<cnt;i++)for(j=1;j<=n;j++)if(~id[j])if(i>>id[j]&1)s[i]+=w[j]; for(i=1;i<=n;i++){ tmp=0; for(j=0;j<1<<cnt;j++)if(~f[i][j])up(tmp,f[i][j]+s[j]); printf("%d\n",tmp); } }
D. 修建道路
不妨设整个序列的最大值位于 $a_x$(若有多个最大值则取最小的 $x$),那么两端点都在 $[1,x-1]$ 或都在 $[x+1,n]$ 的边的边权不会超过 $a_x$,而横跨 $x$ 的边的边权都是 $a_x$,因此最优方案一定是左右两部分都分别连成一个连通块后,再和 $x$ 相连。
于是对于每个 $x$ 计算它需要连多少条边:如果 $a_{x-1}<a_x$,那么左边对应的连通块要和 $x$ 连一条边;如果 $a_{x+1}\leq a_x$,那么右边对应的连通块要和 $x$ 连一条边。因此:\[ans=\sum_{i=1}^{n-1}\max(a_i,a_{i+1})\]
#include<cstdio> int n,i,a[200005];long long ans; int main(){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%d",&a[i]); for(i=2;i<=n;i++)ans+=a[i]>a[i-1]?a[i]:a[i-1]; printf("%lld",ans); }
E. 被遗忘的计划
令每件商品的价值最大值为 $t$,那么选取 $k$ 件商品的价值最大值不超过 $k\times t$;另一方面,我们总可以把 $t$ 对应的商品重复选取 $k$ 次来得到 $k\times t$,因此数组 $f$ 的最大值一定对应 $k\times t$,于是我们得到了 $k$ 的唯一可能取值:即两个数组最大值的商。
得到 $k$ 的唯一可能取值后,使用快速幂求出 $v$ 数组的循环卷积的 $k$ 次幂,判断是否等于 $f$ 数组即可。
时间复杂度 $O(n^2\log k)$。
#include<cstdio> #include<cstdlib> #include<algorithm> using namespace std; typedef long long ll; const int N=1005; const ll inf=1LL<<60; int n,i;ll x,y,k,a[N],f[N],g[N],h[N<<1]; void NO(){ puts("-1"); exit(0); } inline void up(ll&a,ll b){if(a<b)a=b;} void mul(ll a[],ll b[],ll f[]){ int i,j; for(i=0;i<n;i++)h[i]=h[i+n]=-inf; for(i=0;i<n;i++)for(j=0;j<n;j++)up(h[i+j],a[i]+b[j]); for(i=0;i<n;i++)f[i]=max(h[i],h[i+n]); } int main(){ scanf("%d",&n); for(i=1;i<=n;i++)scanf("%lld",&a[i%n]); for(i=0;i<n;i++)scanf("%lld",&f[i]); x=y=-inf; for(i=0;i<n;i++){ x=max(x,a[i]); y=max(y,f[i]); } if(y%x)NO(); y/=x; if(y<1||y>1000000000)NO(); for(i=0;i<n;i++)g[i]=a[i]; for(k=y-1;k;k>>=1,mul(a,a,a))if(k&1)mul(g,a,g); for(i=0;i<n;i++)if(g[i]!=f[i])NO(); printf("%lld",y); }
F. 地图压缩
不难发现行与列是两个独立的问题,因此只需要求出行的最短循环节的长度,再求出列的最短循环节的长度,相乘就是答案。
以行为例,首先通过 Hash 将问题转化为一维问题。一维问题则是经典问题,对于一个长度为 $n$ 的字符串,长度为 $d$ 的前缀是循环节当且仅当长度为 $n-d$ 的前后缀相等,因此需要找到这个字符串最长的前缀,满足该前缀也是该字符串的后缀。可以枚举所有可能的 $d$ 然后使用 Hash $O(1)$ 判断;也可以使用 KMP 算法求出 $nxt$ 数组,答案即为 $n-nxt[n]$。
时间复杂度 $O(n^2+qn)$。
#include<cstdio> typedef unsigned long long ull; const int N=2005,S=10007; int n,q,cnt,i,j,xl,xr,yl,yr,nxt[N]; char a[N][N]; ull p[N],f[N][N],g[N][N],w[N]; inline int cal(){ int i,j=0; for(i=2;i<=cnt;i++){ while(j&&w[j+1]!=w[i])j=nxt[j]; if(w[j+1]==w[i])j++; nxt[i]=j; } return cnt-j; } inline int calx(){ ull base=p[yr-yl+1]; cnt=0; for(int i=xl;i<=xr;i++)w[++cnt]=g[i][yr]-g[i][yl-1]*base; return cal(); } inline int caly(){ ull base=p[xr-xl+1]; cnt=0; for(int i=yl;i<=yr;i++)w[++cnt]=f[xr][i]-f[xl-1][i]*base; return cal(); } int main(){ scanf("%d%d",&n,&q); for(i=1;i<=n;i++)scanf("%s",a[i]+1); for(p[0]=i=1;i<=n;i++)p[i]=p[i-1]*S; for(i=1;i<=n;i++)for(j=1;j<=n;j++){ f[i][j]=f[i-1][j]*S+a[i][j]; g[i][j]=g[i][j-1]*S+a[i][j]; } while(q--){ scanf("%d%d%d%d",&xl,&yl,&xr,&yr); printf("%d\n",calx()*caly()); } }
G. 3G网络
当 $r \rightarrow +\infty$ 时,$n$ 个圆的并趋近于一个半径为 $r$ 的圆,因此答案为 $\frac{1}{n}$。
#include<cstdio> int main(){ int n; scanf("%d",&n); printf("%.15f",1.0/n); }
H. 4G网络
对于每个询问,我们要求的是这 $n$ 个半径为 $r$ 的圆的面积之并(除以它们的总面积 $n\pi r^2$)。
根据微积分的概念,一个区域的面积等价于区域中每个微元累加的结果。注意到所有圆的半径都相等,对于平面中的每个点,它属于圆并当且仅当存在一个圆的圆心到它的距离不超过 $r$。因此对于每个点,我们将其放在离它最近的圆心处考虑,如果离它最近的圆心到它的距离不超过 $r$,那么它需要被计入答案。
枚举每个圆心,找到在 $[-inf,inf]\times[-inf,inf]$ 这个矩形里它作为最近点的管辖区域,容易发现是一个凸多边形,如上图所示。枚举每个圆心,再枚举另一个圆心,可行区域是它们的垂直平分线的一侧,可以使用半平面交在 $O(n^2\log n)$ 时间内预处理出所有 $n$ 个凸多边形,即 Voronoi 图。根据 Voronoi 图的性质,所有凸多边形的边数之和为 $O(n)$。
对于每个询问,枚举一个圆心 $O$,再枚举它管辖区域的凸多边形的一条边 $(A,B)$,那么对答案的贡献为三角形 $OAB$ 与圆 $O$ 的交,可以 $O(1)$ 计算得到。
时间复杂度$O(n^2\log n+qn)$。
#include<cstdio> #include<cmath> #include<algorithm> using namespace std; typedef double ld; const int N=2015; const ld eps=1e-7,inf=30010,pi=acos(-1.0); int n,m,i,j,radius,cl,d[N]; inline int sgn(ld x){ if(x<-eps)return -1; if(x>eps)return 1; return 0; } inline bool Quadratic(ld A,ld B,ld C,ld*t0,ld*t1){ ld discrim=B*B-4.f*A*C; if(discrim<0.)return 0; ld rootDiscrim=sqrt(discrim); ld q; if(B<0)q=-.5f*(B-rootDiscrim); else q=-.5f*(B+rootDiscrim); *t0=q/A;*t1=C/q; if(*t0>*t1)swap(*t0,*t1); return 1; } struct P{ ld x,y; P(){x=y=0;} P(ld _x,ld _y){x=_x,y=_y;} P operator+(const P&b)const{return P(x+b.x,y+b.y);} P operator-(const P&b)const{return P(x-b.x,y-b.y);} P operator*(ld b)const{return P(x*b,y*b);} P operator/(ld b)const{return P(x/b,y/b);} ld operator*(const P&b)const{return x*b.x+y*b.y;} ld len(){return hypot(x,y);} ld len_sqr(){return x*x+y*y;} void read(){ int a,b; scanf("%d%d",&a,&b); x=a,y=b; } }a[N],pool[N][N],p[N]; inline ld cross(const P&a,const P&b){return a.x*b.y-a.y*b.x;} struct Line{ P p,v;ld a; Line(){} Line(const P&_p,const P&_v){p=_p,v=_v;} bool operator<(const Line&b)const{return a<b.a;} void cal(){a=atan2(v.y,v.x);} }line[N],q[N]; inline bool left(const P&p,const Line&l){return cross(l.v,p-l.p)>eps;} inline P pos(const Line&a,const Line&b){ P x=a.p-b.p;ld t=cross(b.v,x)/cross(a.v,b.v); return a.p+a.v*t; } inline void halfplane(P*pool,int&n){ int i,h=1,t=1; for(i=1;i<=cl;i++)line[i].cal(); sort(line+1,line+cl+1); q[1]=line[1]; for(i=2;i<=cl;i++){ while(h<t&&!left(p[t-1],line[i]))t--; while(h<t&&!left(p[h],line[i]))h++; if(fabs(cross(q[t].v,line[i].v))<eps)q[t]=left(q[t].p,line[i])?q[t]:line[i]; else q[++t]=line[i]; if(h<t)p[t-1]=pos(q[t],q[t-1]); } while(h<t&&!left(p[t-1],q[h]))t--; p[t]=pos(q[t],q[h]); n=0; for(i=h;i<=t;i++)pool[n++]=p[i]; } inline ld get_angle(const P&a,const P&b){return fabs(atan2(fabs(cross(a,b)),a*b));} inline P lerp(const P&a,const P&b,ld t){return a*(1-t)+b*t;} inline bool circle_line_intersection(const P&c,const P&a,const P&b,ld*t0,ld*t1){ P d=b-a;ld A=d*d; ld B=d*(a-c)*2.0; ld C=(a-c).len_sqr()-radius; return Quadratic(A,B,C,t0,t1); } inline ld circle_triangle_intersection_area(const P&c,const P&a,const P&b){ if(sgn(cross(a-c,b-c))==0)return 0; static P q[5];int len=0;ld t0,t1;q[len++]=a; if(circle_line_intersection(c,a,b,&t0,&t1)){ if(0<=t0&&t0<=1)q[len++]=lerp(a,b,t0); if(0<=t1&&t1<=1)q[len++]=lerp(a,b,t1); } q[len++]=b;ld s=0; for(int i=1;i<len;i++){ P z=(q[i-1]+q[i])/2; if((z-c).len_sqr()<=radius) s+=fabs(cross(q[i-1]-c,q[i]-c))/2; else s+=radius*get_angle(q[i-1]-c,q[i]-c)/2; } return s; } inline ld circle_polygon_intersection_area(const P&c,P*v,int n){ ld s=0; for(int i=0;i<n;i++){ int j=(i+1)%n; s+=circle_triangle_intersection_area(c,v[i],v[j])*sgn(cross(v[i]-c,v[j]-c)); } return fabs(s); } int main(){ scanf("%d",&n); for(i=0;i<n;i++)a[i].read(); for(i=0;i<n;i++){ line[1]=Line(P(inf,0),P(0,1)); line[2]=Line(P(-inf,0),P(0,-1)); line[3]=Line(P(0,inf),P(-1,0)); line[4]=Line(P(0,-inf),P(1,0)); cl=4; for(j=0;j<n;j++)if(j!=i){ P t=a[j]-a[i]; swap(t.x,t.y); t.x*=-1; line[++cl]=Line((a[i]+a[j])/2,t); } halfplane(pool[i],d[i]); } scanf("%d",&m); while(m--){ scanf("%d",&radius); radius*=radius; ld up=0,down=(pi*radius)*n; for(i=0;i<n;i++)up+=circle_polygon_intersection_area(a[i],pool[i],d[i]); up/=down; printf("%.15f\n",(double)up); } }
I. 驾驶卡丁车
按照题意逐指令逐步模拟即可。
#include<cstdio> const int N=55,M=505; const int dx[8]={-1,-1,-1,0,1,1,1,0},dy[8]={-1,0,1,1,1,0,-1,-1}; int n,m,i,j,cnt,X,Y,D,V; char a[N][N],op[M]; inline bool check(int x,int y,int nx,int ny){ if(nx<1||nx>n||ny<1||ny>m)return 0; if(a[nx][ny]=='#')return 0; if(x==nx||y==ny)return 1; if(a[x][ny]=='#'&&a[nx][y]=='#')return 0; return 1; } int main(){ scanf("%d%d",&n,&m); for(i=1;i<=n;i++)scanf("%s",a[i]+1); scanf("%d%s",&cnt,op+1); for(i=1;i<=n;i++)for(j=1;j<=m;j++)if(a[i][j]=='*')X=i,Y=j; D=1; V=0; for(i=1;i<=cnt;i++){ if(op[i]=='L')D=(D-1+8)%8; else if(op[i]=='R')D=(D+1)%8; else if(op[i]=='U')V++; else if(V)V--; for(j=1;j<=V;j++){ if(!check(X,Y,X+dx[D],Y+dy[D])){ printf("Crash! "); V=0; break; } X+=dx[D],Y+=dy[D]; } printf("%d %d\n",X,Y); } }
J. 最大权边独立集
枚举位于最终边独立集上的加入的边权为 $p$ 的边的数量 $t$,那么 $0\leq t\leq k$ 且 $2t\leq n$,这是因为每条边将占据图中的两个点。
假设最终要加入 $t$ 条边,那么需要从图中删去 $2t$ 个点,然后用 $t\times p\ +$ 剩下图的最大权边独立集来更新答案,这等价于在树上规定 $2t$ 个点不匹配其它点,然后计算树的带权最大匹配。
使用自底向上的树形动态规划来解决这个问题:设 $f[i][j][0]$ 表示考虑了 $i$ 点的子树,$i$ 点的子树内删掉了 $j$ 个点,且 $i$ 不能往上匹配 $i$ 的父亲时的带权最大匹配;设 $f[i][j][1]$ 表示考虑了 $i$ 点的子树,$i$ 点的子树内删掉了 $j$ 个点,且 $i$ 能够往上匹配 $i$ 的父亲时的带权最大匹配。那么状态数为 $O(nk)$,在转移时需要合并两棵子树的信息,$j$ 这一维从 $0$ 开始枚举到 $\min(size_x,k)$ 即可保证时间复杂度为 $O(nk)$,其中 $size_x$ 表示 $x$ 目前的子树大小。
#include<cstdio> #include<algorithm> using namespace std; #define rep(i,n) for(int i=0;i<=n;i++) typedef long long ll; const int N=100005,M=102; int n,m,p,i,x,y,z,g[N<<1],v[N<<1],w[N<<1],nxt[N<<1],ed; int size[N];ll f[N][M*2][2],h[M*2][2],ans; inline void add(int x,int y,int z){v[++ed]=y;w[ed]=z;nxt[ed]=g[x];g[x]=ed;} inline void up(ll&a,ll b){if(a<b)a=b;} void dfs(int x,int y){ rep(j,1)rep(k,1)f[x][j][k]=-1; f[x][0][1]=f[x][1][0]=0; size[x]=1; for(int i=g[x];i;i=nxt[i]){ int u=v[i]; if(u==y)continue; dfs(u,x); int A=size[x],B=size[u],C=A+B,W=w[i]; A=min(A,m),B=min(B,m),C=min(C,m); size[x]+=size[u]; rep(j,C)rep(k,1)h[j][k]=-1; rep(j,A)rep(k,1)if(~f[x][j][k]){ ll F=f[x][j][k]; for(int J=0;J<=B&&j+J<=m;J++)rep(K,1)if(~f[u][J][K]){ up(h[j+J][k],F+f[u][J][K]); if(k&&K)up(h[j+J][0],F+W+f[u][J][K]); } } rep(j,C)rep(k,1)f[x][j][k]=h[j][k]; } } int main(){ scanf("%d%d%d",&n,&m,&p); m*=2; for(i=1;i<n;i++)scanf("%d%d%d",&x,&y,&z),add(x,y,z),add(y,x,z); dfs(1,0); for(i=0;i<=m&&i<=n;i+=2)rep(j,1){ ll tmp=f[1][i][j]; if(tmp<0)continue; up(ans,tmp+1LL*(i/2)*p); } printf("%lld",ans); }
K. 音乐游戏
根据输入数据的合法性,问题等价于统计输入的所有字符串中''-''的个数。一个简单的实现方式是:while(~scanf("%s",s)) 统计 $s$ 中''-''的个数。
#include<cstdio> int i,ans;char s[99]; int main(){ while(~scanf("%s",s))for(i=0;s[i];i++)if(s[i]=='-')ans++; printf("%d",ans); }