AtCoder Grand Contest 022 解题报告。

点此进入比赛

\(A\):Diverse Word(点此看题面

  • 规定一个没有重复字符的字符串为好串。
  • 给定一个好串,求字典序比它大的好串中字典序最小的串。
  • \(|S|\le26\)

签到题

考虑判无解,显然字典序最大的好串是zyx...cba,除此之外都有解。

然后分类讨论。如果一个串长度不足\(26\),只要在其之后加上最小的未出现过的字符即可。

否则,我们从后往前找到第一个存在之后字符比它大的字符,然后找到它后面最小的比它大的字符替换它即可。

代码:\(O(26)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 26
using namespace std;
int n,vis[N+5];char s[N+5];
int main()
{
	RI i,j;if(scanf("%s",s+1),(n=strlen(s+1))^26)//如果串长不足26
	{
		for(i=1;i<=n;++i) vis[s[i]&31]=1;//标记已有字符
		for(i=1;i<=26;++i) if(!vis[i]) {s[n+1]=96+i;break;}//找到最小的未出现字符,加到字符串最后
		return puts(s+1),0;
	}
	for(i=1;i<=26&&s[i]=='z'-i+1;++i);if(i>26) return puts("-1"),0;//判无解
	for(i=25;s[i]>s[i+1];--i);for(j=i+1;j<=26&&s[j]>s[i];++j);--j;//找到最后面的可替换位置,用最小的字符替换
	return s[i]=s[j],s[i+1]=0,puts(s+1),0;
}

\(B\):GCD Sequence(点此看题面

  • 求构造一个长度为\(n\)的序列,其中没有重复的元素,且所有元素不超过\(3\times10^4\)
  • 要求满足所有元素\(gcd=1\),且任一元素同整个序列之和的\(gcd\not=1\)
  • \(n\le2\times10^4\)

构造

发现\(3\times10^4\)以内\(2\)\(3\)的倍数恰好有\(2\times10^4\)个。

所以我们考虑让整个序列的总和为\(6\)的倍数,同时使得序列中的数既有偶数也有奇数,既有\(3\)的倍数也有非\(3\)的倍数。

那么只要保证加入\(3\)的倍数的个数是偶数个就可以保证是\(2\)的倍数。

至于保证其为\(3\)的倍数,可以考虑先不加入\(6\),最后根据整个序列和模\(3\)的余数把\(2\)\(4\)修改为\(6\)。(但注意当\(n\)很大且为偶数时需要把所有偶数都加入)

代码:\(O(nlogn)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 20000
#define V 30000
using namespace std;
int n;set<int> S;set<int>::iterator it;
int main()
{
	#define Ins(x) (S.insert(x),++t,s+=x)
	RI i,t=0;long long s=0;if(scanf("%d",&n),n==3) return puts("2 5 63"),0;//特判小数据
	for(i=1;2*i<=V&&t^(n-2);++i) ((n>V/2&&n&1^1)||i^3)&&Ins(i<<1);for(i=1;t^n;i+=2) Ins(3*i);//分别加入偶数和3的倍数
	if(s%3==1) S.erase(4),S.insert(6);if(s%3==2) S.erase(2),S.insert(6);//调整使得总和为6的倍数
	for(it=S.begin();it!=S.end();++it) printf("%d ",*it);return 0;//输出序列中的元素
}

\(C\):Remainder Game(点此看题面

  • 给定两个长度为\(n\)的序列\(a,b\)
  • 每次操作你可以选定一个数\(k\),花费\(2^k\)的代价,任选若干\(a_i\)\(k\)取模。
  • 求最少花费多少代价把\(a\)变成\(b\)
  • \(n,a_i,b_i\le50\)

贪心

显然,一个\(k\)不可能被选中两次,否则完全可以归在一次操作里。

然后考虑\(k\)的操作代价为\(2^k\),所以我们即便操作再多小的\(k\),也优于操作一个大的\(k\)

因此,我们从大到小枚举每一个\(k\),强制其不选看是否依然能达到要求。

至于如何判断能否达到要求,反正\(n\)这么小,直接\(O(n^3)\)暴力\(DP\)即可。

代码:\(O(n^4)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 50
using namespace std;
int n,a[N+5],b[N+5],f[N+5],used[N+5];
I bool Check(CI x)//检验能否不选x
{
	RI i,j,k;for(i=0;i<=50;++i) f[i]=0;for(i=1;i<=n;++i)//枚举每一个元素
	{
		for(f[a[i]]=i,j=a[i];j>=b[i];--j) if(f[j]==i) for(k=1;k<=j;++k) (k<x||used[k])&&(f[j%k]=i);//暴力DP
		if(f[b[i]]^i) return 0;//如果转移不到,说明不能不选x
	}return 1;
}
int main()
{
	RI i;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i);
	for(i=1;i<=n;++i) if(scanf("%d",b+i),a[i]^b[i]&&a[i]<=2*b[i]) return puts("-1"),0;//判无解
	long long t=0;for(i=50;i;--i) !Check(i)&&(used[i]=1,t|=1LL<<i);return printf("%lld\n",t),0;//从大到小枚举每一个k
}

\(D\):Shopping(点此看题面

  • 一根数轴上有\(n\)个商店,坐标\(x_i\),逛此商店用时\(t_i\)
  • 一辆火车从\(0\)出发,每单位时间移动一个单位长度,在\(0\)\(L\)之间无限往返。
  • 你从\(0\)出发,每到一个商店可以下车,当火车再次经过时可以重新上车。
  • 问逛完所有商店返回原点的最少用时。
  • \(n\le3\times10^5\)

暴力题解

首先,我们可以将\(t_i\)\(2L\)取模,肯定不影响操作策略。且这样一来可以保证\(t_i\in[0,2L]\),方便了许多。

然后,我们将所有\(t_i\not=0\)的站点分成四类:

  1. 在从左侧或右侧经过时下车,在下一次经过时都能直接上车。
  2. 仅在从左侧经过时下车,在下一次经过时能直接上车。
  3. 仅在从右侧经过时下车,在下一次经过时能直接上车。
  4. 在从左侧或右侧经过时下车,在下一次经过时都不能直接上车。

我们分别考虑这四类站点。

首先发现对于第四类站点,可以发现它与\(t_i=0\)的站点没有什么区别,可以直接将其看作\(t_i=0\),同时将最终答案加上\(2L\)。但要注意,\(t_i=0\)的站点不能直接忽略,因为我们至少需要经过它们一次,例如最后一个站点必须要走到。

然后考虑第二类站点,如果在从右侧经过时下车,需要等\(2L\)的时间才能上车,其实等价于在车上等到从左侧经过时下车。因此可以强制第二类站点仅从左侧下车,第三类同理。

显然第二类站点一定在第三类站点左边,也就是说简化之后站点序列形如\(2,2,...,2,3,3...,3,4\),其中\(1\)可以随意看作\(2\)\(3\)

那么只要把\(2\)视作\(-1\)\(3\)视作\(1\),就可以得到一个\(O(n^2)\)\(DP\)解法了。

\(O(n)\)的做法不会,坑掉了。

代码:\(O(n^2)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 3000
#define LL long long
#define Gmin(x,y) (x>(y)&&(x=(y)))
using namespace std;
int n,L,x[N+5],t[N+5],f[N+5][N+5];
int main()
{
	RI i,j;for(scanf("%d%d",&n,&L),i=1;i<=n;++i) scanf("%d",x+i);
	LL g=0;for(i=1;i<=n;++i) scanf("%d",t+i),
		g+=t[i]/(2*L),(t[i]%=2*L)>max(2*x[i],2*(L-x[i]))&&(t[i]=0,++g);//第四类点看作t=0,给答案加2L
	for(i=0;i<=n;++i) for(j=0;j<=n;++j) f[i][j]=1e9;//初始化
	for(f[0][0]=0,i=1;i<=n;++i) for(j=0;j<=n;++j)//DP
	{
		if(f[i-1][j]==1e9) continue;//无效状态
		if(!t[i]) {j?Gmin(f[i][j],f[i-1][j]):Gmin(f[i][j+1],f[i-1][j]+1);continue;}//t[i]=0
		t[i]<=2*(L-x[i])&&(j?Gmin(f[i][j-1],f[i-1][j]):Gmin(f[i][j],f[i-1][j]+1)),//第二类(第一类)
		t[i]<=2*x[i]&&(j?Gmin(f[i][j+1],f[i-1][j]+1):Gmin(f[i][j+2],f[i-1][j]+2));//第三类(第一类)
	}
	LL s=1e9;for(j=0;j<=n;++j) Gmin(s,f[n][j]);return printf("%lld\n",(s+g)*2*L),0;//统计答案并输出
}

\(E\):Median Replace(点此看题面

  • 给定一个长度为\(n\)\(01\)串(\(n\)为奇数),其中有一些?可以任填。
  • 每次操作可以把连续三个字符替换为它们的中位数。
  • 问有多少种填法可以使得到的串在\(\frac{n-1}2\)次操作之后能够变成1
  • \(n\le3\times10^5\)

解题思路

显然的贪心,每有三个连续的0,就把它们合并为一个0

然后考虑10的合并,虽然可能有很多情况,但简单归纳一下就是用一个1拼掉一个0

因此这是类似于维护一个栈结构,每次加入一个元素:

  • 加入1:如果栈顶是0则弹出栈顶,否则1入栈。
  • 加入0:如果栈顶已经有\(2\)0则弹出栈顶,否则0入栈。(注意0不会主动拼掉1

显然栈中只可能是一连串1与一两个0

那么很容易得到\(DP\)状态\(f_{i,j,k}\)表示\(DP\)到第\(i\)位,栈中有\(j\)1\(k\)0的方案数。

因为0的个数只可能是\(0/1/2\),总复杂度\(O(n^2)\)

临门一脚

好不容易做到这一步,考虑优化这个\(DP\),结果思考很久都没啥想法,只好点开题解。

发现就差临门一脚,真的可惜。。。

考虑当某一时刻栈中1的个数达到两个了,因为无论如何0的个数都不会超过两个,更多的1也没用了,因此完全可以把1超过两个的情况归到两个的情况中

于是就变成\(O(n)\)了。。。

具体\(DP\)转移可见代码,有注释。

代码:\(O(n)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 300000
#define X 1000000007
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,f[N+5][3][3];char s[N+5];
int main()
{
	RI i,j;for(scanf("%s",s+1),n=strlen(s+1),f[0][0][0]=i=1;i<=n;++i)//DP
	{
		if(s[i]^'1') for(j=0;j<=2;++j)//加入0
			f[i][j][1]=(f[i-1][j][0]+f[i-1][j][2])%X,f[i][j][2]=f[i-1][j][1];
		//没有0或是两个0都能转移到1个0,只有1个0才能转移到2个0
		if(s[i]^'0') for(Inc(f[i][2][0],f[i-1][2][0]),j=0;j<=2;++j)//加入1,注意超过2个1归到两个1的状态
			Inc(f[i][j][1],f[i-1][j][2]),Inc(f[i][j][0],f[i-1][j][1]),j&&Inc(f[i][j][0],f[i-1][j-1][0]);
		//拼掉1个0,或是增加1个1
	}
	RI t=0;for(j=0;j<=2;++j) Inc(t,f[n][j][0]),j>=1&&Inc(t,f[n][j][1]),j>=2&&Inc(t,f[n][j][2]);//统计答案,最终1的个数要比0多
	return printf("%d\n",t),0;
}

\(F\):Checkers(点此看题面

  • \(X=10^{100}\),一开始有\(n\)个点坐标分别为\(X^i\)
  • 每次操作选中两个点,将一个点以另一个点为对称点翻转,并删去该对称点。
  • \(n-1\)次操作后剩下的点坐标可能有多少种。
  • \(n\le50\)

题解

一个点的坐标是非常巨大的,因此只需要关系每个坐标的系数。

所以,两个方案不同当且仅当存在一个\(x_i\)使得它在两个方案里的系数不同。

考虑两点的合并,实际上就是一个坐标乘\(2\),一个坐标乘\(-1\)

故一个\(x_i\)最终的系数必然可以表示为\((-1)^p\times2^k\)的形式,确定了系数中\(2^k\)\(-2^k\)的个数即可统计答案,而答案就是一个可重排列组合数

我们把点的合并看成一个建树的过程,则两点合并时要新建一个点,向这两点分别连一条边权为\(2\)\(-1\)的边。最终得到的树中每个叶节点对应\(x_i\)的系数就是它到根节点路径权值之积。

因为不同的建树过程可能得到相同的树,所以此题我们要从上往下考虑,做点的拆分。

一个节点向下连出一条\(2\)的边和一条\(-1\)的边,把一个\((-1)^p\times2^k\)拆成\((-1)^p\times2^{k+1}\)\((-1)^{p+1}\times2^k\)

一开始根节点为\((-1)^0\times2^0\),然后我们设\(f_{i,x,y}\)表示当前有\(i\)个叶节点,当前幂次有\(x\)个系数为负、\(y\)个系数为正。

枚举分裂了\(a\)个负的和\(b\)个正的,就可以得到转移:

\[f_{i+x+y,a,b}\texttt{+=}\frac{f_{i,x,y}}{(x-a+b)!\times(y+a-b)!} \]

但此时的\(DP\)\(O(n^5)\)的。

仔细观察转移式,发现\(a,b\)\(a-b\)的形式同时出现,因此只要记录状态\(a-b\)即可。

代码:\(O(n^4)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 50
#define X 1000000007
using namespace std;
int n,f[N+5][2*N+5],Fac[N+5],IFac[N+5];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
int main()
{
	RI i,j;for(scanf("%d",&n),Fac[0]=i=1;i<=n;++i) Fac[i]=1LL*Fac[i-1]*i%X;//预处理阶乘
	for(IFac[n]=QP(Fac[n],X-2),i=n-1;~i;--i) IFac[i]=1LL*IFac[i+1]*(i+1)%X;//预处理阶乘逆元
	#define DP(x,y) for(j=-(y);j<=(x);++j)\
		f[i+(x)+(y)][N+j]=(1LL*IFac[(x)-j]*IFac[(y)+j]%X*f[i][N+(x)-(y)]+f[i+(x)+(y)][N+j])%X//一次转移的过程
	RI x,y;for(f[i=0][N+1]=1;i^n;++i) if(!i) DP(1,0);//特殊处理i=0时的转移
		else for(x=0;x<=n-i;++x) for(y=0;y<=n-i-x;++y) if(x+y>0) DP(x,y);//枚举x,y转移
	return printf("%d\n",1LL*Fac[n]*f[n][N]%X),0;//输出答案时记得乘上总排列数
}
败得义无反顾,弱得一无是处