CSP-S 2022 第二轮 题解

求点赞

T1. 假期计划 (holiday)

这个题作为t1可一点都不简单啊。

先用bfs \(O(nm)\)求出任意两点之间的最短路,这样就可以判断哪些点对可以排在一起。注意到第1、4个景点是对称的,第2、3个景点是对称的。我们对于每个景点求出当它作为第2个景点时,第1个景点的可能集合,令第i个点的这个集合为\(s_i\)。接下来枚举我们最终走的路径的第2和第3个景点x和y,接下来需要选择第1、4个景点是的分数最大且景点不重复。我们拿出\(s_x\)中分数最大的3个景点,以及\(s_y\)中分数最大的3个景点。发现如果有解,最优解一定在这几个景点的\(3*3=9\)种组合里,对于每对\((x,y)\)依次枚举这9种即可。

时间复杂度\(O(nm+9n^2)\)

#include <bits/stdc++.h>

#define rep(i,n) for(int i=0;i<n;++i)
#define repn(i,n) for(int i=1;i<=n;++i)
#define LL long long
#define pii pair <LL,LL>
#define fi first
#define se second
#define mpr make_pair
#define pb push_back

void fileio()
{
  #ifdef LGS
  freopen("in.txt","r",stdin);
  freopen("out.txt","w",stdout);
  #endif
}
void termin()
{
  #ifdef LGS
  std::cout<<"\n\nPROGRAM TERMINATED";
  #endif
  exit(0);
}

using namespace std;

const LL inf=1e18;

LL n,m,k,a[2510],dist[2510][2510];
pii mx[2510][3];
vector <LL> g[2510];

void bfs(LL s)
{
  queue <LL> q;
  dist[s][s]=0;q.push(s);
  while(!q.empty())
  {
    int f=q.front();q.pop();
    rep(i,g[f].size()) if(dist[s][g[f][i]]>=inf)
    {
      dist[s][g[f][i]]=dist[s][f]+1;
      q.push(g[f][i]);
    }
  }
}

void upd(LL i,LL j,pii val)
{
  if(j>2) return;
  if(val.fi<mx[i][j].fi)
  {
    upd(i,j+1,val);
    return;
  }
  swap(mx[i][j],val);
  upd(i,j+1,val);
}

int main()
{
  fileio();
  freopen("holiday.in","r",stdin);
  freopen("holiday.out","w",stdout);

  cin>>n>>m>>k;++k;
  for(int i=2;i<=n;++i) scanf("%lld",&a[i]);
  LL x,y;
  rep(i,m)
  {
    scanf("%lld%lld",&x,&y);
    g[x].pb(y);g[y].pb(x);
  }
  rep(i,n+3) rep(j,n+3) dist[i][j]=inf;
  repn(i,n) bfs(i);
  repn(i,n) rep(j,3) mx[i][j]=mpr(-inf,-inf);
  for(int i=2;i<=n;++i)
  {
    for(int j=2;j<=n;++j) if(j!=i&&dist[1][j]<=k&&dist[i][j]<=k)
      upd(i,0,mpr(a[j],j));
  }
  LL ans=0;
  for(int i=2;i<=n;++i) for(int j=i+1;j<=n;++j) if(dist[i][j]<=k)
  {
    rep(p,3) if(mx[i][p].fi>-inf&&mx[i][p].se!=j)
      rep(r,3) if(mx[j][r].fi>-inf&&mx[j][r].se!=i&&mx[j][r].se!=mx[i][p].se)
				ans=max(ans,mx[i][p].fi+mx[j][r].fi+a[i]+a[j]);
  }
  cout<<ans<<endl;

  termin();
}

T2. 策略游戏 (game)

方便起见称先手为A,后手为B。

先看单独的一组询问。如果A选了第i行,那么B肯定会选择使得\(C_{i,j}\)最小的可选的j,令这个\(C_{i,j}\)\(v_i\)。A不傻,他肯定会选择所有可选的\(v_i\)中最大的。

进一步分析。先假设A选择的\(a_i\)必须是非负数,那么B一定会在所有可选的\(b_j\)里选最小的。这时候A就要考虑考虑了,如果可选的最小的\(b_j\)是负数,那么A在非负的\(a_i\)里应该选最小的才能使\(C_{i,j}\)最大;如果可选的最小的\(b_j\)是非负数,A应该选最大的非负\(a_i\)。当强制A选择负数时,情况与之对称。

所以我们先讨论A选的是非负数还是负数,然后按照\(b\)中的最大/小值的正负性来判断A的选择。所有操作都可以通过线段树或ST表完成。

时间复杂度\(O((n+q)logn)\)

#include <bits/stdc++.h>

#define rep(i,n) for(int i=0;i<n;++i)
#define repn(i,n) for(int i=1;i<=n;++i)
#define LL long long
#define pii pair <LL,LL>
#define fi first
#define se second
#define mpr make_pair
#define pb push_back

using namespace std;

void fileio()
{
  #ifdef LGS
  freopen("in.txt","r",stdin);
  freopen("out.txt","w",stdout);
  #endif
}
void termin()
{
  #ifdef LGS
  std::cout<<"\n\nPROGRAM TERMINATED";
  #endif
  exit(0);
}

const LL inf=1e18;

LL n,m,q,a[100010],b[100010];

struct SegTree
{
  LL n2,mx[400010],mn[400010];
  void initl(LL nn)
  {
    n2=1;while(n2<nn) n2*=2;
    rep(i,n2+n2+3) mx[i]=-inf,mn[i]=inf;
  }
  void build(LL k,LL lb,LL ub)
  {
    if(lb==ub) return;
    build(k+k+1,lb,(lb+ub)/2);build(k+k+2,(lb+ub)/2+1,ub);
    mx[k]=max(mx[k+k+1],mx[k+k+2]);mn[k]=min(mn[k+k+1],mn[k+k+2]);
  }
  pii qry(LL k,LL lb,LL ub,LL tlb,LL tub)
  {
    if(ub<tlb||tub<lb) return mpr(inf,-inf);
    if(tlb<=lb&&ub<=tub) return mpr(mn[k],mx[k]);
    LL mid=(lb+ub)>>1;
    pii l=qry(k+k+1,lb,mid,tlb,tub),r=qry(k+k+2,mid+1,ub,tlb,tub);
    return mpr(min(l.fi,r.fi),max(l.se,r.se));
  }
}lp,ln,r;

int main()
{
  fileio();
  freopen("game.in","r",stdin);
  freopen("game.out","w",stdout);

  cin>>n>>m>>q;
  rep(i,n) scanf("%lld",&a[i]);
  rep(i,m) scanf("%lld",&b[i]);
  lp.initl(n);ln.initl(n);r.initl(m);
  rep(i,n)
  {
    if(a[i]>=0) lp.mx[i+lp.n2-1]=lp.mn[i+lp.n2-1]=a[i];
    else ln.mx[i+ln.n2-1]=ln.mn[i+ln.n2-1]=a[i];
  }
  rep(i,m) r.mx[i+r.n2-1]=r.mn[i+r.n2-1]=b[i];
  lp.build(0,0,lp.n2-1);ln.build(0,0,ln.n2-1);r.build(0,0,r.n2-1);
  LL x,y,xx,yy;
  rep(qn,q)
  {
    scanf("%lld%lld%lld%lld",&x,&y,&xx,&yy);--x;--y;--xx;--yy;
    LL ans=-inf;
    pii Rres=r.qry(0,0,r.n2-1,xx,yy);
    pii res=lp.qry(0,0,lp.n2-1,x,y);//{mn,mx}
    if(res.fi<inf)//有非负数
    {
      if(Rres.fi<0) ans=max(ans,Rres.fi*res.fi);
      else ans=max(ans,Rres.fi*res.se);
    }
    res=ln.qry(0,0,ln.n2-1,x,y);
    if(res.fi<inf)//有负数
    {
      if(Rres.se>0) ans=max(ans,Rres.se*res.se);
      else ans=max(ans,Rres.se*res.fi);
    }
    printf("%lld\n",ans);
  }

  termin();
}

T3. 星战 (galaxy)

诈骗题,有点随机区分了。

注意到题面很复杂,但是要我们输出的却是有YES和NO。这样判断是否合法的题很多都是能用哈希做的,以后碰到这种应该多往哈希上想。

发现一个图合法的充要条件就是每个点的出度都为1(基环内向树)。考虑使用XOR-hashing,把合法的条件转化为总边数为n,且每个点的出度都是奇数。在修改的过程中记录每个点的出度和所有点总出度是很简单的,没有问题。对每一个节点赋一个随机权值val,一个节点每多一条出边就令\(res = res \ xor \ val_i\),最后判断res是否等于所有点的权值异或和即可。

题目中的操作有四种,其中两种是只修改一条边;还有两种是覆盖操作,把一个点的所有入边都修改成同一个状态(有或无)。在对一个点进行的两次覆盖操作之间,不管进行了多少次单点修改,不管这个点的入边状态变得多乱,等到下一次覆盖操作的时候大家又变得一样了,都被覆盖掉了。所以每个单点修改操作只会最多被"覆盖"一次。我们对每个点开两个set,good和bad,分别维护它的入边中存在的和被摧毁的。假设现在我们要进行覆盖操作,把它的所有入边都改为存在,分两种情况:

  1. good的大小大于bad,把bad中所有的元素都移到good中即可
  2. good的大小小于等于bad,把good中所有的元素都移到bad中,然后swap(good,bad)。swap两个set是\(O(1)\)

根据上面说的每个单点操作只会被覆盖一次的性质,我们"移动元素"的操作只会做最多\(O(n)\)次。这样就可以过了。

时间复杂度\(O((n+q)logn)\)

这题的60分暴力很好写的,有一个群友t4一直调不出来,最后t3压根没写,翻车了,血亏。引以为戒

#include <bits/stdc++.h>

#define rep(i,n) for(int i=0;i<n;++i)
#define repn(i,n) for(int i=1;i<=n;++i)
#define LL long long
#define pii pair <int,int>
#define fi first
#define se second
#define mpr make_pair
#define pb push_back

void fileio()
{
  #ifdef LGS
  freopen("in.txt","r",stdin);
  freopen("out.txt","w",stdout);
  #endif
}
void termin()
{
  #ifdef LGS
  std::cout<<"\n\nPROGRAM TERMINATED";
  #endif
  exit(0);
}

using namespace std;

mt19937_64 rnd(114514);
LL Rand(){return rnd()%1000000000000000000;}
LL n,m,q,h[500010],ecnt=0,tot[500010],cur;
set <LL> good[500010],bad[500010];

void toBad(LL x)
{
  for(auto it:good[x])
  {
    bad[x].insert(it);
    --ecnt;cur^=h[it];
  }
  good[x].clear();
}
void toGood(LL x)
{
  for(auto it:bad[x])
  {
    good[x].insert(it);
    ++ecnt;cur^=h[it];
  }
  bad[x].clear();
}

void swit(LL x)
{
  if(bad[x].empty())//good->bad
  {
    ecnt-=good[x].size();cur^=tot[x];
    swap(good[x],bad[x]);
  }
  else
  {
    ecnt+=bad[x].size();cur^=tot[x];
    swap(good[x],bad[x]);
  }
}

int main()
{
  fileio();

  cin>>n>>m;
  LL need=0;
  repn(i,n) h[i]=Rand(),need^=h[i];
  LL x,y;
  rep(i,m)
  {
    scanf("%lld%lld",&x,&y);
    tot[y]^=h[x];cur^=h[x];++ecnt;
    good[y].insert(x);
  }
  cin>>q;
  rep(qn,q)
  {
    scanf("%lld",&x);
    if(x==1)
    {
      scanf("%lld%lld",&x,&y);
      good[y].erase(x);bad[y].insert(x);--ecnt;
      cur^=h[x];
    }
    else if(x==3)
    {
      scanf("%lld%lld",&x,&y);
      bad[y].erase(x);good[y].insert(x);++ecnt;
      cur^=h[x];
    }
    else if(x==2)
    {
      scanf("%lld",&x);
      if(good[x].size()<bad[x].size()) toBad(x);
      else toGood(x),swit(x);
    }
    else
    {
      scanf("%lld",&x);
      if(good[x].size()<bad[x].size()) toBad(x),swit(x);
      else toGood(x);
    }
    puts((ecnt==n&&cur==need) ? "YES":"NO");
  }

  termin();
}

T4. 数据传输 (transmit)

某种程度上说这题比t3简单吧。

k=1的情况,我们要求的就是树上两点间路径权值和,前缀和一下就行了。

k=2的情况,发现我们是不可能走到起点和终点间简单路径的外面的:


k=3的情况,我们会走到路径外面,但是只会走到离路径距离=1的点,而且只有下面两种走法:

以k=3为例。先看单独的一组询问,比如我们询问x到y的最小代价。拿出x到y的简单路径\(v_1 \cdots v_k,v_1=x,v_k=y\),令\(a_i\)表示点\(v_i\)的代价,\(b_i\)表示到点\(v_i\)距离不超过1的点的最小代价。令\(dp_{i,0}\)表示遍历到第i个点,上一个落脚点就是点\(v_i\)的最小代价;\(dp_{i,1}\)表示上一个落脚点是\(v_{i-1}\)的代价;\(dp_{i,2}\)表示上一个落脚点是点\(v_{i-2}\)的最小代价;\(dp_{i,3}\)表示上一个落脚点不在简单路径上,而在一个到\(v_{i-1}\)距离为1的点上的最小代价。转移关系有以下几条:

\[\begin{align} dp_{i,0}&\leftarrow dp_{i-1,0/1/2/3}\\ dp_{i,1}&\leftarrow dp_{i-1,0}\\ dp_{i,2}&\leftarrow dp_{i-1,1}\\ dp_{i,3}&\leftarrow dp_{i-1,2},dp_{i-1,3}(想想上面图片里的仅有的两种走法) \end{align} \]

发现这个转移可以写成一个矩阵乘法的形式,只是把正常矩阵乘法中的*改成+,把+改成取min。不放心的话可以写个程序验证一下,发现这种"矩阵乘法"也满足结合律等性质。这启发我们用倍增,树上每个节点都拥有一个矩阵,我们对树上的每个点求出从它开始,向上1、2、4、8\(\cdots\)个点的矩阵乘积,每次询问时先求出两个点分别到它们lca的矩阵的乘积,然后再在lca处把它们的dp值合并。k=3时合并是比较复杂的,为避免分类讨论,我直接把lca附近的7个点拿出来跑了一个小型的dp。这个矩阵的方法在k=1和k=2时也是通用的,改一下矩阵的内容就行了。

现在矩阵的边长是4,有点太大了,其实发现可以把\(dp_{*,2}和dp_{*,3}\)合并,完全不影响转移。

注意树上一个节点拥有的矩阵是从它转移到它父亲时需要乘上的矩阵。

时间复杂度\(O(3^3nlogn)\)

#include <bits/stdc++.h>

#define rep(i,n) for(int i=0;i<n;++i)
#define repn(i,n) for(int i=1;i<=n;++i)
#define LL long long
#define pii pair <int,int>
#define fi first
#define se second
#define mpr make_pair
#define pb push_back

void fileio()
{
  #ifdef LGS
  freopen("in.txt","r",stdin);
  freopen("out.txt","w",stdout);
  #endif
}
void termin()
{
  #ifdef LGS
  std::cout<<"\n\nPROGRAM TERMINATED";
  #endif
  exit(0);
}

using namespace std;

const LL inf=1e18;

LL n,q,t,a[200010],dep[200010],fa[200010][23],d1[200010],mat[200010][23][3][3],res[2][3][3];
vector <LL> g[200010];

void chmin(LL &x,LL y){if(x>y) x=y;}

void dfs(LL pos,LL par,LL d)
{
  dep[pos]=d;fa[pos][0]=par;
  rep(i,g[pos].size()) if(g[pos][i]!=par) dfs(g[pos][i],pos,d+1);
}

LL getLCA(LL x,LL y)
{
  for(int i=19;i>=0;--i) if(fa[x][i]>0&&dep[fa[x][i]]>=dep[y]) x=fa[x][i];
  for(int i=19;i>=0;--i) if(fa[y][i]>0&&dep[fa[y][i]]>=dep[x]) y=fa[y][i];
  if(x==y) return x;
  for(int i=19;i>=0;--i) if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i];
  return fa[x][0];
}

void mul(LL aa[3][3],LL bb[3][3],LL cc[3][3])
{
  rep(i,t) rep(j,t) cc[i][j]=inf;
  rep(i,t) rep(j,t) rep(k,t) chmin(cc[i][j],aa[i][k]+bb[k][j]);
}
void mul(LL aa[3][3],LL bb[3][3])
{
  LL cc[3][3];
  rep(i,t) rep(j,t) cc[i][j]=inf;
  rep(i,t) rep(j,t) rep(k,t) chmin(cc[i][j],bb[i][k]+aa[k][j]);
  rep(i,t) rep(j,t) aa[i][j]=cc[i][j];
}

void getMat(LL x,LL stp,LL w)
{
  res[w][0][0]=-1;
  rep(i,19) if(stp&(1<<i))
  {
    if(res[w][0][0]==-1) rep(j,t) rep(k,t) res[w][j][k]=mat[x][i][j][k];
    else mul(res[w],mat[x][i]);
    x=fa[x][i];
  }
}

LL getFa(LL x,LL stp)
{
  if(stp<0) return -1;
  rep(i,19) if(stp&(1<<i)) x=fa[x][i];
  return x;
}

int main()
{
  fileio();

  cin>>n>>q>>t;
  repn(i,n) scanf("%lld",&a[i]);
  LL x,y;
  rep(i,n-1)
  {
    scanf("%lld%lld",&x,&y);
    g[x].pb(y);g[y].pb(x);
  }
  repn(i,n)
  {
    d1[i]=a[i];
    rep(j,g[i].size()) d1[i]=min(d1[i],a[g[i][j]]);
  }
  dfs(1,0,0);
  rep(i,20) repn(j,n) fa[j][i+1]=fa[fa[j][i]][i];
  repn(i,n)
  {
    auto it=mat[i][0];
    if(t==1) it[0][0]=a[fa[i][0]];
    else if(t==2)
    {
      it[0][0]=it[0][1]=a[fa[i][0]];
      it[1][0]=0;it[1][1]=inf;
    }
    else
    {
      rep(j,3) it[0][j]=a[fa[i][0]];
      it[1][0]=0;it[1][1]=it[1][2]=inf;
      it[2][0]=inf;it[2][1]=0;it[2][2]=d1[i];
    }
  }
  rep(i,20) repn(j,n)
  {
    if(fa[j][i]==0) rep(k,t) rep(p,t) mat[j][i+1][k][p]=mat[j][i][k][p];
    else mul(mat[fa[j][i]][i],mat[j][i],mat[j][i+1]);
  }
  rep(qn,q)
  {
    scanf("%lld%lld",&x,&y);
    if(dep[x]>dep[y]) swap(x,y);
    LL lca=getLCA(x,y);
    if(x==lca)
    {
      if(dep[y]==dep[x]+1) printf("%lld\n",a[x]+a[y]);
      else
      {
        getMat(y,dep[y]-dep[x]-1,0);
        LL ans=inf;rep(i,t) chmin(ans,res[0][i][0]);
        ans+=a[x]+a[y];
        printf("%lld\n",ans);
      }
    }
    else
    {
      getMat(x,dep[x]-dep[lca]-1,0);getMat(y,dep[y]-dep[lca]-1,1);
      LL aa[3],bb[3];
      if(res[0][0][0]==-1) aa[0]=a[x],aa[1]=aa[2]=inf;
      else rep(i,t) aa[i]=res[0][i][0]+a[x];
      if(res[1][0][0]==-1) bb[0]=a[y],bb[1]=bb[2]=inf;
      else rep(i,t) bb[i]=res[1][i][0]+a[y];
      if(t==1) printf("%lld\n",aa[0]+bb[0]+a[lca]);
      else if(t==2)
      {
        LL ans=inf;
        rep(i,2) rep(j,2) chmin(ans,aa[i]+bb[j]+((i==0&&j==0) ? 0LL:a[lca]));
        printf("%lld\n",ans);
      }
      else
      {
        LL fdx=dep[x]-dep[lca],fdy=dep[y]-dep[lca];
        vector <LL> v={getFa(x,fdx-3),getFa(x,fdx-2),getFa(x,fdx-1),lca,getFa(y,fdy-1),getFa(y,fdy-2),getFa(y,fdy-3)};
        LL dp[10][2];
        rep(i,10) rep(j,2) dp[i][j]=inf;
        dp[0][0]=aa[2];dp[1][0]=aa[1];dp[2][0]=aa[0];
        rep(i,7)
        {
          for(int j=i+1;j<=i+3;++j) if(j<7&&v[j]>-1) chmin(dp[j][0],dp[i][0]+a[v[j]]);
          for(int j=i+1;j<=i+2;++j) if(j<7&&v[j]>-1) chmin(dp[j][1],dp[i][0]+d1[v[j]]);
          if(i+2<7&&v[i+2]>-1) chmin(dp[i+2][0],dp[i][1]+a[v[i+2]]);
          if(i+1<7&&v[i+1]>-1) chmin(dp[i+1][1],dp[i][1]+d1[v[i+1]]);
        }
        LL ans=inf;
        if(v[4]>-1) chmin(ans,dp[4][0]+bb[0]-a[v[4]]);
        if(v[5]>-1) chmin(ans,dp[5][0]+bb[1]-a[v[5]]);
        if(v[6]>-1) chmin(ans,dp[6][0]+bb[2]-a[v[6]]);
        if(v[5]>-1) chmin(ans,dp[5][1]+bb[2]-d1[v[5]]);
        printf("%lld\n",ans);
      }
    }
  }

  termin();
}

总体比去年简单一些,但是我以及周围不少人都考得不是很好,感觉区分度不太够啊,尤其t3。
但是毕竟csp嘛,不爆零就是胜利

posted @ 2022-11-02 11:53  LegendStane  阅读(2048)  评论(0)    收藏  举报