Codeforces Round 1020 (Div. 3) CF 2106 A~G2 题解

人生第一次打过jiangly

点我看题

A. Dr. TC

原串中每一个\(1\)最终的出现次数是\(n-1\),而每个\(0\)最终的出现次数是\(1\)。因此直接统计即可。

时间复杂度\(O(n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n;
string s;

int main()
{
  //fileio();

  cin>>t;
  while(t--)
  {
    cin>>n>>s;
    int ans=0;
    rep(i,n) if(s[i]=='1') ans+=n-1;else ++ans;
    cout<<ans<<endl;
  }

  return 0;
}

B. St. Chroma

为了让\(x\)尽早作为MEX出现,我们先把小于\(x\)的元素一股脑塞在序列最前面。塞完这些元素之后MEX就是\(x\)了,为了保持,我们接下来优先放\(>x\)的元素,把\(x\)放最后即可。注意特判\(x=n\)的情况。

时间复杂度\(O(n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n,x;

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n>>x;
    if(n==x)
    {
      rep(i,n) cout<<i<<' ';
      cout<<endl;
      continue;
    }
    rep(i,x) cout<<i<<' ';
    for(int i=x+1;i<n;++i) cout<<i<<' ';
    cout<<x<<endl;
  }

  return 0;
}

C. Cherry Bomb

首先如果\(b\)中有\(\neq -1\)的元素,那\(ab\)中对应的两个数之和\(x\)的值就已经确定了,对\([1,n]\)中每个位置检查是否合法即可。如果\(b\)中只有-1,易发现\(x\)的取值范围是\([max\{a_i\},k-min\{a_i\}]\),直接计算即可。

时间复杂度\(O(n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n,k,a[200010],b[200010];

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n>>k;
    rep(i,n) cin>>a[i];
    rep(i,n) cin>>b[i];
    int sum=-1,ok=1;
    rep(i,n) if(b[i]>-1)
    {
      if(sum==-1) sum=a[i]+b[i];
      else if(sum!=a[i]+b[i]) ok=0;
    }
    if(ok==0)
    {
      cout<<0<<endl;
      continue;
    }
    if(sum>-1)
    {
      rep(i,n) if(b[i]==-1&&sum-a[i]>k||sum-a[i]<0) ok=0;
      cout<<ok<<endl;
    }
    else
    {
      int mxa=-1,mna=1e9;
      rep(i,n)
      {
        mxa=max(mxa,a[i]);
        mna=min(mna,a[i]);
      }
      cout<<mna+k-mxa+1<<endl;
    }
  }

  return 0;
}

D. Flower Boy

下面解析中数组下标从1开始。

如果序列\(a\)已经固定,我们想要检查是否能摘到足够的花,可以采用从开头开始贪心匹配\(a,b\)序列的方法。即如果\(a_i\geq b_j\),其中\(j-1\)是目前摘的花的数量,那就摘下这朵花并向前走一步;否则,直接向前走一步。方便起见,我们将一对序列\((a_0,b_0)\)使用这种贪心方法摘到的花的数量称为匹配数

首先对原序列\((a,b)\)做一次这种匹配,如果匹配数为\(m\),则答案为0。否则,令\(pref_i(i\in[0,n])\)表示\(a\)的前\(i\)个元素与\(b\)的匹配数,\(suf_i(i\in[0,n])\)表示 \(a\)的后\(i\)个元素组成的反向后的序列 与 \(b\)反向后的序列 的匹配数。若插入一个beauty\(=k\)的花能解决问题的话,注意到必存在\(i\)满足\(pref_i+suf_{n-i}=m-1\),我们此时需要把新的花插入在第\(i\)盆花之后,且它的beauty\(\leq b_{pref_i+1}\)。若这样的\(i\)不存在,则无解。

时间复杂度\(O(n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n,m,a[200010],b[200010],pref[200010],suf[200010];

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n>>m;
    rep(i,n) cin>>a[i];
    rep(i,m) cin>>b[i];
    
    pref[0]=(a[0]>=b[0] ? 1:0);
    repn(i,n-1)
    {
      if(pref[i-1]==m) pref[i]=m;
      else if(a[i]>=b[pref[i-1]]) pref[i]=pref[i-1]+1;
      else pref[i]=pref[i-1];
    }

    suf[n-1]=(a[n-1]>=b[m-1] ? 1:0);
    for(int i=n-2;i>=0;--i)
    {
      if(suf[i+1]==m) suf[i]=m;
      else if(a[i]>=b[m-suf[i+1]-1]) suf[i]=suf[i+1]+1;
      else suf[i]=suf[i+1];
    }

    if(pref[n-1]==m)
    {
      cout<<0<<endl;
      continue;
    }
    int ans=INT_MAX;
    if(pref[n-1]==m-1) ans=b[m-1];
    if(suf[0]==m-1) ans=min(ans,b[0]);
    rep(i,n-1) if(pref[i]+suf[i+1]==m-1) ans=min(ans,b[pref[i]]);
    if(ans==INT_MAX) cout<<-1<<endl;
    else cout<<ans<<endl;
  }

  return 0;
}

E. Wolf

下面解析中数组下标从1开始。

\(pos_i\)表示值\(i\)在原序列中的位置。对于一次询问,若\(pos_x\notin[l,r]\),则显然无解。否则,我们模拟一下在\([l,r]\)上二分的过程,发现其中存在\(O(log_2n)\)个"关键位置",即每次二分中的\(m\),这些位置上的值是决定二分是否成功的关键。具体来说,对于其中一些位置我们要求其上的值\(>x\),方便起见我们称之为B位;对于另一些位置我们要求其上的值\(<x\),称之为S位

\([1,n]\)\(>x\)的数的数量小于B位的数量,则无论我们如何操作,都不可能达成目标,此时直接判无解。对于S位也是类似。

对于位置上的数原本就\(>x\)的B位以及位置上的数原本就\(<x\)的S位(称它们为好位),我们不去动他们是最好的,比较省操作。对于位置上的数\(<x\)的B位以及位置上的数\(>x\)的S位(称它们为坏位),令其数量依次为\(b,s\)。称除了好位、坏位和\(pos_x\)的位置为普通位。注意到,我们需要从普通位中叫一些"外援",把这些外援划到我们选择出来并shuffle的\(d\)个元素中。观察发现至少需要叫\(|b-s|\)个外援才能满足所有B/S位的"要求",并且一定能选出来(选不出来的情况已经在前面被判无解了)。因此答案为\(2\cdot max(b,s)\)

时间复杂度\(O(n+qlog_2n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n,q,p[200010],pos[200010];

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n>>q;
    repn(i,n) cin>>p[i],pos[p[i]]=i;
    while(q--)
    {
      int l,r,x;
      cin>>l>>r>>x;
      int targ=pos[x],big=n-x,sml=x-1,nbig=0,nsml=0,nsbig=0,nssml=0;
      if(targ<l||targ>r)
      {
        cout<<-1<<' ';
        continue;
      }
      while(l<r)
      {
        int mid=(l+r)/2;
        if(mid==targ) break;
        if(mid<targ)
        {
          ++nsml;
          if(p[mid]>x) ++nsbig;
          l=mid+1;
        }
        else
        {
          ++nbig;
          if(p[mid]<x) ++nssml;
          r=mid-1;
        }
      }
      if(nbig>big||nsml>sml)
      {
        cout<<-1<<' ';
        continue;
      }
      cout<<max(nsbig,nssml)*2<<' ';
    }
    cout<<endl;
  }

  return 0;
}

F. Goblin

想象一条向右移动的"扫描线",每次刷新出这个矩阵的一列。注意到每一列中要么只有一个0,要么只有一个1。令扫描线上一个位置的答案表示在扫描线及其左侧,包括这个位置的最大的全0连通块的大小(若这个位置的值为1,则答案为0)。同时,对于扫描线上连续的一堆值为0的位置,它们的答案相同。因此任意时刻,可以把扫描线分成三个区间,其中中间区间的长度为1,满足每个区间中各位置的答案相同。令这三个区间的答案为\(dp_{i,0/1/2}\)(随便吧,怎么表示都行)。然后在扫描线相邻的两个位置之间就可以用非常无脑的方式进行\(dp\)转移,这里不再赘述。最终答案对所有\(dp\)值取最大值即可。

时间复杂度\(O(n)\)

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

LL t,n;
string s;

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n>>s;
    LL l,m,r,ans;
    if(s[0]=='1') m=1,l=r=0,ans=1;
    else l=m=0,r=n-1,ans=n-1;
    repn(i,n-1)
    {
      LL u=i,d=n-i-1;
      if(s[i-1]=='1'&&s[i]=='1') l=r=0,m=1;
      else if(s[i-1]=='1'&&s[i]=='0') l=m+u,m=0,r=d;
      else if(s[i-1]=='0'&&s[i]=='1') l=0,m=r+1,r=0;
      else l+=u,m=0,r+=d;
      ans=max(ans,max(l,m));
      if(i<n-1) ans=max(ans,r);
    }
    cout<<ans<<endl;
  }

  return 0;
}

G2. Baudelaire (hard version)

n+200次操作?还是在上?这一眼就是个重心分治相关。

注意到如果我们已经知道了根的位置,我们只需要对每个结点做一次\(k=1\)的1类型询问,就可以算出每个点的权值。因此考虑怎么使用200次左右的操作找到根的位置。

我们挑出树上的任意一条边来观察一下。令其直接连接的点为\(a,b\)。将这条边切断,树就分为了 与\(a\)连接的点 和 与\(b\)连接的点 两个集合。方便起见,称其为 \(a\)子树中的点 和 \(b\)子树中的点。如何判断树根在哪个子树中呢?很简单,我们先对\(a\)子树中所有点做一次1询问,接下来用2询问改变\(b\)的权值,然后再对\(a\)子树中所有点做一次1询问,如果两次1询问结果的差值绝对值是 \(a\)子树中结点个数\(\times 2\),则根在\(b\)子树中,否则在\(a\)子树中。这是因为只有在询问1中涉及的每个结点到根的路径都经过权值被改变的结点时,询问结果差值绝对值才会\(=\)询问结点个数\(\times2\)

那么能不能每次切断一条边直到根可能位于的结点只剩一个呢?不能,因为不存在一个叫边分治的东西,随便来一个菊花图你复杂度就爆炸了。

考虑重心分治,每轮递归尝试判断根位于重心的哪个子树内,或者是不是就是重心本身。分以下几种情况:

  • 连通块只剩一个点。那根就是重心本身。
  • 重心(在当前连通块内)只有一个子树。那说明连通块内只有2个点,对重心本身做一次1询问,若结果为1或-1则重心是根,否则另一个点是根。
  • 重心有不止一个子树。可能出现以下两种情况:
    • 重心不是根。考虑在一堆子树中进行二分,每次用类似上面"判断根在\(a\)的子树还是\(b\)的子树中"的方法来判断根在 左边的一堆子树中 还是 右边的一堆子树中(即"先对左边一堆子树中所有点做一次1询问,接下来用2询问改变重心的权值,然后再对左边一堆子树中所有点做一次1询问"),此时我们可以知道根是否在左边一堆子树中,因此可以缩小二分范围。一轮递归使用的询问数量大约为\(3\cdot log_2(重心的子树数量)\)
    • 重心是根。在上面那种情况的第一次二分之前,我们先用相同的方法判断一下,根有没有一种可能,我是说可能,不在左边的一堆子树中,也不在右边的一堆子树中。这时候说明重心就是根。

这样就优雅地找到了根。接下来用\(n\)次询问问出每个结点的权值就行了。

总操作数大约\(n+O(log_2^2n)\),反正肯定是在限制内的。

其实如果再出恶心一点的话,可以强制要求你"利用"上面找根过程中的询问,来代替掉这\(n\)个询问中的一部分。我瞎说的。

点击查看代码
#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 pb push_back
#define mpr make_pair

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

using namespace std;

int t,n,sz[1010],pref[1010],ans[1010],root;
vector <int> g[1010],subtroots,tmp;
vector <vector <int> > subts;
bool vis[1010];

int qry(vector <int> nds)
{
  cout<<"? 1 "<<nds.size()<<' ';
  rep(i,nds.size()) cout<<nds[i]<<' ';
  cout<<endl;
  cout.flush();
  int ret;cin>>ret;return ret;
}
void qry2(int pos)
{
  cout<<"? 2 "<<pos<<endl;
  cout.flush();
}

void getSz(int pos,int par)
{
  sz[pos]=1;
  rep(i,g[pos].size()) if(!vis[g[pos][i]]&&g[pos][i]!=par)
  {
    getSz(g[pos][i],pos);
    sz[pos]+=sz[g[pos][i]];
  }
}
pii getMn(int pos,int par,int tot)//{max subtree size, node}
{
  int mx=1e9,cursz=tot-sz[pos];
  pii ret={mx,114514};
  rep(i,g[pos].size()) if(!vis[g[pos][i]]&&g[pos][i]!=par)
  {
    ret=min(ret,getMn(g[pos][i],pos,tot));
    cursz=max(cursz,sz[g[pos][i]]);
  }
  ret=min(ret,{cursz,pos});
  return ret;
}
void getNodes(int pos,int par)
{
  tmp.pb(pos);
  rep(i,g[pos].size()) if(!vis[g[pos][i]]&&g[pos][i]!=par) getNodes(g[pos][i],pos);
}

bool isInOther(int lb,int ub,int par)
{
  int tot=pref[ub+1]-pref[lb];
  vector <int> nds;
  for(int i=lb;i<=ub;++i) rep(j,subts[i].size()) nds.pb(subts[i][j]);
  int res=qry(nds);
  qry2(par);
  int res2=qry(nds);
  return abs(res-res2)==tot*2;
}

int findRoot(int pos)
{
  getSz(pos,-1);pos=getMn(pos,-1,sz[pos]).se;
  subts.clear();subtroots.clear();
  rep(i,g[pos].size()) if(!vis[g[pos][i]])
  {
    tmp.clear();
    getNodes(g[pos][i],pos);
    subts.pb(tmp);
    subtroots.pb(g[pos][i]);
  }
  rep(i,subts.size()) pref[i+1]=pref[i]+subts[i].size();
  if(subts.size()==0) return pos;
  if(subts.size()==1)
  {
    int res=qry({pos});
    if(res==1||res==-1) return pos;
    else return subts[0][0];
  } 
  int lb=0,ub=subts.size()-1,mid;
  while(lb<ub)
  {
    mid=(lb+ub)/2;
    bool res=isInOther(0,mid,pos);
    if(lb==0&&ub==subts.size()-1)
    {
      bool res2=isInOther(mid+1,ub,pos);
      if(res&&res2) return pos;
    }
    if(res) lb=mid+1;
    else ub=mid;
  }
  vis[pos]=true;
  return findRoot(subtroots[lb]);
}

void getAns(int pos,int par,int parval)
{
  int res=qry({pos});
  ans[pos]=res-parval;
  rep(i,g[pos].size()) if(g[pos][i]!=par) getAns(g[pos][i],pos,res);
}

int main()
{
  //fileio();

  ios::sync_with_stdio(0);

  cin>>t;
  while(t--)
  {
    cin>>n;
    rep(i,n+5) g[i].clear(),vis[i]=false;
    int x,y;
    rep(i,n-1)
    {
      cin>>x>>y;
      g[x].pb(y);
      g[y].pb(x);
    }
    root=findRoot(1);
    getAns(root,0,0);
    cout<<"! ";
    repn(i,n) cout<<ans[i]<<' ';
    cout<<endl;
    cout.flush();
  }

  return 0;
}
posted @ 2025-04-25 01:48  LegendStane  阅读(587)  评论(0)    收藏  举报