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);
}

  

posted @ 2022-01-14 22:52  Claris  阅读(616)  评论(0编辑  收藏  举报