点我看题

对两三年前的cf进行考古的时候偶然做到这场,像这种全是构造题和思维题的比赛还是比较少见的。题目本身很有意思,但是对于现场打比赛想上分的人来说体验就比较差了。

A. Subsequence Permutation

先把原串s排序,令排完序的串为t。注意到s和t不相等的那些位置是必须被修改的;而直接把这些位置拿出来排个序,发现就已经能把s变成t了。所以直接统计s和t有多少个位置不同即可。

时间复杂度\(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 fi first
#define se second
#define pb push_back
#define mpr make_pair

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

int t,n;
string s;

int main()
{
  fileio();

  cin>>t;
  rep(tn,t)
  {
    cin>>n>>s;
    string ss=s;
    sort(ss.begin(),ss.end());
    int ans=0;
    rep(i,n) if(s[i]!=ss[i]) ++ans;
    cout<<ans<<endl;
  }

  termin();
}

B. Running for Gold

题目渐渐变得思维起来了

我们从前到后遍历所有的选手,把它们一个个放进当前参赛选手的集合。同时维护一个集合中"最可能拿到金牌"的选手编号。维护方法如下:加入一个选手i时,令当前最可能拿到金牌的选手编号为opt,如果i有至少三场比赛比opt强,就令opt=i,否则opt不变。实际上,任何时刻opt都是集合中唯一可能拿到金牌的选手。证明可以归纳,假设opt是当前集合中唯一可能拿到金牌的选手,当加入一个选手i时,如果它有至少三场比opt强,那opt就不可能拿到金牌;否则,i就不可能拿到金牌。

最后再检查一下opt与所有选手想必是否都能有至少三场更强就行了,如果不是那就是无解。时间复杂度\(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 fi first
#define se second
#define pb push_back
#define mpr make_pair

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

int t,n,r[50010][10];

bool isBetter(int i,int j)
{
  int cc=0;
  rep(k,5) if(r[i][k]<r[j][k]) ++cc;
  return cc>=3;
}

int main()
{
  fileio();

  cin>>t;
  rep(tn,t)
  {
    cin>>n;
    rep(i,n) rep(j,5) scanf("%d",&r[i][j]);
    int good=0;
    repn(i,n-1) if(isBetter(i,good)) good=i;
    bool bad=false;
    rep(i,n) if(i!=good&& !isBetter(good,i)) bad=true;
    if(bad) puts("-1");
    else cout<<good+1<<endl;
  }

  termin();
}

C. Maximize the Intersections

首先要知道(a,b)和(c,d)两条线段相交,当且仅当[a,b]和[c,d]两个区间相交(\(a<b,c<d\))。

已经存在的线段之间的交点数是已经确定的,单独统计一下就行。令\(lft=n-k\)。对每一条已经存在的线段(a,b)(a<b),令c表示区间(a,b)中初始没被连到的点数,则在剩余没被添加的线段中,最多有\(min(c,lft-c)\)条与这条线段相交。还没添加的线段中,最好的情况是两两之间都有交点。

但其实这两个最大值可以同时达到,把所有没匹配的点按照顺时针顺序拿出来,从0开始编号,把第i个与第\((i+lft)mod\ 2lft\)个匹配即可。

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

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

int t,n,k,x[110],y[110],mark[210];

int main()
{
  fileio();

  cin>>t;
  rep(tn,t)
  {
    cin>>n>>k;
    rep(i,n+n+3) mark[i]=0;
    rep(i,k)
    {
      cin>>x[i]>>y[i];--x[i];--y[i];
      mark[x[i]]=mark[y[i]]=1;
      if(x[i]>y[i]) swap(x[i],y[i]);
    }
    int ans=(n-k)*(n-k-1)/2;
    rep(i,k) for(int j=i+1;j<k;++j) if((x[i]<x[j]&&x[j]<y[i]&&y[i]<y[j])||(x[j]<x[i]&&x[i]<y[j]&&y[j]<y[i])) ++ans;
    int tot=n+n-k-k;
    rep(i,k)
    {
      int cc=0;
      for(int j=x[i]+1;j<y[i];++j) if(mark[j]==0) ++cc;
      cc=min(cc,tot-cc);
      ans+=cc; 
    }
    cout<<ans<<endl;
  }

  termin();
}

D. Array Differentiation

首先当a中有0的时候一定是有解的。然后把a中的负数全都取绝对值,因为这里正数和负数是一样的。

我们可以把数组b看成一张图,第i个点的权值为\(b_i\)。在两个点i和j之间可以连一条权值为\(|b_i-b_j|\)的边。我们要求权值为\(a_1\cdots a_n\)的边都在这个图里出现,问存不存在这样的图。

既然点和边都是n个,那如果存在合法的图,其中一定存在环,存在环也就一定有解。所以有解的充要条件就是:能从a中取出一些边使他们构成一个环。也就是a中存在两个不相交的集合,满足两个集合的权值和相等。这个枚举一下mask就行了。

时间复杂度\(O(3^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 fi first
#define se second
#define pb push_back
#define mpr make_pair

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

int t,n,a[20],sum[1100];

int main()
{
  fileio();

  cin>>t;
  rep(tn,t)
  {
    cin>>n;
    rep(i,n) cin>>a[i];
    if(n==1)
    {
      cout<<(a[0]==0 ? "YES":"NO")<<endl;
      continue;
    }
    bool ok=false;
    rep(i,n) if(a[i]==0) ok=true;else a[i]=abs(a[i]);
    if(ok) puts("YES");
    else
    {
      rep(i,1<<n)
      {
        sum[i]=0;
        rep(j,n) if(i&(1<<j)) sum[i]+=a[j];
      }
      rep(msk,1<<n)
      {
        int full=((1<<n)-1)^msk;
        for(int msk2=full;msk2>0;msk2=(msk2-1)&full) if(sum[msk]==sum[msk2])
        {
          //cout<<msk<<' '<<msk2<<' '<<sum[msk]<<' '<<sum[msk2]<<endl;
          ok=true;
          break;
        }
        if(ok) break;
      }
      puts(ok ? "YES":"NO");
    }
  }

  termin();
}

E. Colors and Intervals

脑筋急转弯题

首先令\(pos_{i,j}\)表示数值i在数组中的第j次出现的位置。注意到每个数值一定是选相邻的两次出现作为a和b最优。假设现在已经求出了答案,按照每个数值i在最后选取的区间\([a_i,b_i]\),我们可以把数值分类:选第一次和第二次出现的,选第二次和第三次的,\(\cdots\) ,选第\(k-1\)次和第\(k\)次的。令\(B=\lceil \frac n{k-1} \rceil\),现在我们以B个为一组,给每个组分配对应的数值。从左到右依次遍历每个组(12,23,...,k-1和k 这个顺序),遍历到第i组时,每次取出还没有分配组别的数值,把他们按照\(pos_{val,i+1}\)从小到大排序,并选最小的B个作为这组的成员,如果不足B个就全选。发现这样构造,不同的两组之间的区间绝对不会相交,每个组内部的位置也最多被覆盖B次,所以是符合题目要求的。

时间复杂度\(O(n^2logn)\)

点击查看代码
#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

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

int n,k,c[10010],mark[110];
pii ans[110];
vector <int> pos[110];

int main()
{
  fileio();

  cin>>n>>k;
  rep(i,n*k)
  {
    cin>>c[i];--c[i];
    pos[c[i]].pb(i);
  }
  int b=(n+k-2)/(k-1);
  rep(i,k-1)
  {
    vector <pii> cand;
    rep(j,n) if(!mark[j]) cand.pb(mpr(pos[j][i+1],j));
    sort(cand.begin(),cand.end());
    rep(j,min((int)cand.size(),b))
    {
      mark[cand[j].se]=1;
      ans[cand[j].se]=mpr(pos[cand[j].se][i],pos[cand[j].se][i+1]);
    }
  }
  rep(i,n) cout<<ans[i].fi+1<<' '<<ans[i].se+1<<endl;

  termin();
}

F. Telepanting

虽然难度不高,但是题目的想法很有创造性

从前往后做似乎是不太好做的,因为蚂蚁的路线一直在循环往复,找不出规律。我们试试从后往前做。

对于每个传送门,我们观察它的起点(\(x_i\))会被经过多少次。对于最后一个传送门n,如果\(s_n=0\)则只会经过1次,就是从前往后正常走的那次;否则会经过两次,第一次正常经过的时候会被传送回去,第二次经过的时候门已经关了,所以正常通过。在\(s_n=1\)的情况中,蚂蚁被往回传送了1次,所以它会给在\((y_n,x_n)\)范围内的所有传送门的终点都增加1次"正常经过"的次数。观察发现,一个传送门i的终点被"正常经过"的次数是1+后面的传送门把蚂蚁传送到它前面的次数。而这个传送门把蚂蚁往前传送的次数是 正常经过次数+\(s_i\)-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 fi first
#define se second
#define pb push_back
#define mpr make_pair

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

const LL MOD=998244353;

LL n,x[200010],y[200010],s[200010],add[200010],ans;

int main()
{
  fileio();

  cin>>n;
  rep(i,n) scanf("%lld%lld%lld",&x[i],&y[i],&s[i]);
  ans=(x[n-1]+1)%MOD;
  LL cur=1;
  for(int i=n-1;i>=0;--i)
  {
    (cur+=add[i])%=MOD;
    LL bk=(s[i]==1 ? cur:(cur+MOD-1)%MOD);
    (ans+=bk*(x[i]-y[i]))%=MOD;
    (cur+=bk)%=MOD;
    LL p=lower_bound(x,x+n,y[i])-x-1;
    if(p>=0) (add[p]+=MOD-bk)%=MOD;
  }
  cout<<ans<<endl;

  termin();
}

G. A Serious Referee

令"第i步的操作集合"表示\(\{j_{i,1},j_{i,2}\cdots j_{i,q_i}\}\)

首先1-n的每个位置都必须被至少一个操作集合覆盖,否则肯定能构造出一个无法排序的序列。

接下来进行两步转化。我们要判断输入的这组操作是否能排序任意序列,其实也就是判断是否能排序任何 元素两两不同 的序列,这是显然的,因为如果有两个元素相同,你可以给它们任意定一个大小关系。进一步转化,能排序任何元素两两不同的序列 的充要条件其实是:能排序任何只由0和1组成的序列。必要性是显然的,来证明一下充分性:对于任意一个两两不同的序列,以及任意\(1\le i<n\) ,你把序列中最大的i个元素看成1,其余的看成0,由于已知这个操作序列可以排序任何01序列,那么这最大的i个元素一定会被排在最后面,并且对所有i都成立,所以这个两两不同的序列一定能被排序。

定义一个进行完i步的"合法状态"为一个bitmask(长度为n),满足存在一个初始01序列使得进行前i步操作后得到这个bitmask。令\(T_i\)表示所有进行完i步的合法状态的集合。发现答案为ACCEPTED当且仅当\(T_n\)中的所有元素都形如\(\{0,0,0\cdots 1,1,1\}\)(所有0在前,所有1在后)。

进一步观察发现每个\(T_i\)的大小都不会特别大。令\(cur_i\)表示前i步操作覆盖到的所有位置的集合(\(cur_n=\{1,2,3\cdots n\}\)),\(radd_i\)表示\(cur_{i+1}\)\(cur_i\)多出的部分(写成位运算也就是\(cur_i \bigoplus cur_{i+1}\))。注意到\(T_i\)中的每个状态只能转移到\(|radd_i|+1\)\(T_{i+1}\)中的状态,转移到什么状态取决于\(radd_i\)中的位置上有多少个0多少个1。因此\(|T_{i+1}|\le |T_i|\cdot (|radd_i|+1)\)\(|T_n|\)最大的情况肯定是\(radd\)的大小分布得尽量均匀的情况,因此\(|T_n|\le (\frac{n+k}k)^k\),是不大的。

所以按照上面的做法直接模拟,算出\(T_n\)中的所有元素就行了。几个时间优化:转移的时候不要去重,去重复杂度很高,重复了是没关系的;用bitmask和位运算处理集合操作。

点击查看代码
#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

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\nEXECUTION TERMINATED";
  #endif
  exit(0);
}

LL n,k,msks[2][10000000],len[2];
vector <LL> v[20];

int main()
{
  fileio();

  cin>>n>>k;
  rep(i,k)
  {
    LL x,y;
    scanf("%lld",&x);
    rep(j,x)
    {
      scanf("%lld",&y);--y;
      v[i].pb(y);
    }
  }
  if(n==1)
  {
    puts("ACCEPTED");
    termin();
  }
  LL cur=0;
  len[0]=1;msks[0][0]=0;
  rep(ii,k)
  {
    LL i=ii&1;
    LL add=0,radd,coll;
    rep(j,v[ii].size()) add|=(1LL<<v[ii][j]);
    LL svcur=cur;cur|=add;
    radd=cur^svcur;coll=add&svcur;
    len[i^1]=0;
    LL more=__builtin_popcountll(radd);
    vector <LL> bits;rep(j,n) if(add&(1LL<<j)) bits.pb(j);
    vector <LL> sv=bits;
    rep(j,len[i])
    {
      bits=sv;
      LL mskv=msks[i][j],ori1=__builtin_popcountll(mskv&coll);
      mskv&=((1LL<<n)-1-add);
      rep(k,ori1) mskv|=(1LL<<bits.back()),bits.pop_back();
      rep(new1,more+1)
      {
        msks[i^1][len[i^1]++]=mskv;
        if(bits.size()) mskv|=(1LL<<bits.back()),bits.pop_back();
      }
    }
  }
  if(cur!=(1LL<<n)-1) puts("REJECTED"),termin();
  bool ok=true;
  int ni=k&1;
  rep(i,len[ni])
  {
    LL msk=msks[ni][i];
    rep(j,n-1) if((msk&(1LL<<j))>0&&(msk&(1LL<<(j+1)))==0)
    {
      ok=false;
      break;
    }
    if(!ok) break;
  }
  puts(ok ? "ACCEPTED":"REJECTED");

  termin();
}