AtCoder Grand Contest 022

Preface

这场太菜了恢复正常水平,只做出ABC+E(前面全推出来了就最后一步脑抽了),DF都不会


A - Diverse Word

刚开始还石乐志错了一发太蠢了

首先样例就提示我们当\(n<26\)时直接在末尾加一个没出现过的最小的字符即可

否则当\(n=26\)时,我们就从后往前枚举在那个位置开始截断,要保证截断这个位置之后能在后面处理过的部分找到一个更大的字符放在这里

#include<cstdio>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=30;
int n; char s[N]; bool vis[N];
int main()
{
	RI i,j,k; for (scanf("%s",s+1),n=strlen(s+1),i=1;i<=n;++i) vis[s[i]-'a']=1;
	if (n<26)
	{
		for (printf("%s",s+1),i=0;i<26;++i) if (!vis[i]) return putchar(i+'a'),0;
	}
	for (i=n;i;--i)
	{
		for (j=s[i]+1;j<='z';++j) if (!vis[j-'a'])
		{
			for (k=1;k<i;++k) putchar(s[k]); putchar(j); return 0;
		}
		vis[s[i]-'a']=0;
	}
	return puts("-1"),0;
}

B - GCD Sequence

ORZ构造之神陈指导

我们先考虑到\(\gcd(x_i,sum-x_i)=\gcd(x_i,sum)\),然后最朴素的一个想法就是把\(2\)的倍数都扔进去

但这样显然可能不满足所有数\(\gcd=1\)的限制,于是我们又很容易想到把\(3\)的倍数也扔进去

我们可以强制\(2,3\)都在集合中,现在就要满足集合中所有数的和为\(6\)的倍数

我们发现对于所有\(2\)的倍数,从模\(6\)\(2\)的开始,相邻的两个一组一定是\(6\)的倍数:$2+4=6,8+10=18,\cdots $

对于所有\(3\)的倍数,从模\(6\)\(3\)的开始,每三个跳一个取两个一定是\(6\)的倍数:$3+9=12,15+21=36,\cdots $

因此我们再强制每次加数都两个一组得加即可,如果不够的话显然还预留了所有\(6\)的倍数,可以直接加入

注意特判\(n=3\)的情况……

由于\(30000\)以内的\(2,3\)的倍数刚好有\(20000\)个,可以完美地卡着过去

#include<cstdio>
#include<cctype>
#define RI register int
#define CI const int&
using namespace std;
const int S=30000;
int n,num; bool vis[S+5];
int main()
{
	RI i; scanf("%d",&n); if (n==3) return puts("2 5 63"),0;
	vis[2]=vis[3]=vis[4]=vis[9]=1; num=4;
	for (i=8;i+2<=S&&num+2<=n;i+=6) vis[i]=vis[i+2]=1,num+=2;
	for (i=15;i+6<=S&&num+2<=n;i+=12) vis[i]=vis[i+6]=1,num+=2;
	for (i=6;i<=S&&num+1<=n;i+=6) vis[i]=1,++num;
	for (i=1;i<=S;++i) if (vis[i]) printf("%d ",i);
	return 0;
}

C - Remainder Game

我们发现对于每种\(k\),每个数至多模一次\(k\),因为\(\sum_{j=1}^ {i-1} 2^j<2^k\),因此对于大的\(k\)我们肯定能不取就不取

我们从大到小枚举\(k\),每次判断把所有\([1,k)\)的操作都做了的情况下能否达成条件

如何判断一个数经历某些操作之后能否变成目标数,乍一看好像能难处理,但由于数据范围很小我们每次可以直接\(O(n^3)\)DP出来

因此总复杂度\(O(n^4)\),可以通过此题

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=55;
int n,a[N],b[N],c[N],tot; long long ans; bool f[N][N];
inline bool check(CI lim)
{
	RI i,j,k; for (i=0;i<=50;++i) for (j=0;j<=50;++j) f[i][j]=0;
	for (i=0;i<=50;++i) f[i][i]=1;
	for (i=0;i<=50;++i) for (j=50;~j;--j)
	{
		for (k=1;k<=lim;++k) f[i][j%k]|=f[i][j];
		for (k=1;k<=tot;++k) f[i][j%c[k]]|=f[i][j];
	}
	for (i=1;i<=n;++i) if (!f[a[i]][b[i]]) return 0; 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) scanf("%d",&b[i]);
	for (i=1;i<=n;++i) if (a[i]!=b[i]&&b[i]>=(a[i]+1>>1)) return puts("-1"),0;
	for (i=50;i;--i) if (!check(i-1)) ans|=1LL<<i,c[++tot]=i;
	return printf("%lld",ans),0;
}

D - Shopping

首先考虑将所有\(t_i\)\(2L\)取模,此时\(t_i\in [0,2L)\)

我们发现我们可以讲所有\(t_i\ne 0\)的站点分为以下四类:

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

考虑对于第4种站点,我们可以直接把答案加上\(2L\)然后把它变为\(t_i=0\)的站点,因此可以直接删除不用考虑(除了在末尾的情况,因此此时我们必须经过它)

考虑对于一个第2类站点,如果在右侧经过时下车需要等\(2L\)才能重新上车,我们不妨认为他直接在车上等到从左侧经过时再下车,这样第2类站点就强制只能从左侧下车了

对于第\(3\)类站点也是同理,仅可以在从右侧经过时再下车

然后我们注意到第2类站点一定在第三类站点之前,即站点序列形如\(2,2,\cdots,2,3,3\cdots,3,(4)\),并且在中间的任意位置可以出现第1类站点,而第1类站点可以被任意指定为第2类或第3类

因此我们可以把第2类站点看做\(-1\),把第\(3\)类站点看做\(+1\),就可以用一个暴力DP解决

\(f_{i,j}\)表示做到第\(i\)的站点,如上统计站点的前缀和为\(j\)的最小开车轮次,注意当\(j=0\)时需要让车重新开出因此贡献的计算会略有不同

总复杂度\(O(n^2)\)\(O(n)\)的做法完全看不懂,打个1000pts走人

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=3005,INF=1e9;
int n,l,x[N],t[N],f[N][N],cur,ans;
int main()
{
	RI i,j; for (scanf("%d%d",&n,&l),i=1;i<=n;++i)
	scanf("%d",&x[i]); for (i=1;i<=n;++i)
	{
		scanf("%d",&t[i]); cur+=t[i]/(2*l); t[i]%=2*l;
		if (t[i]>2*max(x[i],l-x[i])) t[i]=0,++cur;
	}
	for (i=0;i<=n;++i) for (j=0;j<=n;++j) f[i][j]=INF;
	for (f[0][0]=i=0;i<n;++i) for (j=0;j<=n;++j) if (f[i][j]!=INF)
	{
		if (!t[i+1])
		{
			if (j) f[i+1][j]=min(f[i+1][j],f[i][j]);
			else f[i+1][j+1]=min(f[i+1][j+1],f[i][j]+1); continue;
		}
		if (t[i+1]<=2*(l-x[i+1])) //come from left
		{
			if (j) f[i+1][j-1]=min(f[i+1][j-1],f[i][j]);
			else f[i+1][j]=min(f[i+1][j],f[i][j]+1);
		}
		if (t[i+1]<=2*x[i+1]) //come from right
		{
			if (j) f[i+1][j+1]=min(f[i+1][j+1],f[i][j]+1);
			else f[i+1][j+2]=min(f[i+1][j+2],f[i][j]+2);
		}
	}
	for (ans=INF,i=0;i<=n;++i) ans=min(ans,f[n][i]);
	return printf("%lld",2LL*l*(ans+cur)),0;
}

E - Median Replace

MD每次怎么都可以这么nc把近在眼前的结论放跑掉……

首先我们发现连续的一段\(0\)肯定都是可以预先操作掉的,这样显然不会更劣

考虑带有\(1\)时操作\(111\)显然是非常nt的,因此只有两种情况\(001\to 0,110\to 1\)

不难发现以上的两种情况可以等价为:减少一对相邻的\(01\)

我们考虑用一个类似栈的结构进行贪心

每次加入\(0\)时,若栈顶是\(0\)就判断下达到\(3\)个连续的\(0\)就改成\(1\)\(0\),若栈顶是\(1\)就直接加在栈顶

若加入的是\(1\)时,若栈顶是\(0\)显然直接和这个\(0\)抵消掉不会更劣,若栈顶是\(1\)就直接加在栈顶

容易发现此时栈的元素一定是连续的一段\(1\),后面接上不超过\(2\)\(0\)

我们显然可以设计一个DP\(f_{i,j,0/1/2}\)表示前\(i\)个位置,栈中的\(1\)\(j\)个,\(0\)\(k\)个的方案数,转移十分显然

然后和陈指导想了好久的优化无果,遂点开题解发现自己就是个绝世大傻逼

因为栈中的\(1\)一旦加入就不会减少,因此当\(1\)的个数达到两个之后就一定有解了(因为\(0\)的个数不会超过\(2\)个,\(1\)个个数大于等于\(0\)的个数就有解)

或者你直接对应要原问题上也很好理解,当你对一段前缀操作得到只剩下连续的\(2\)\(1\)时,对剩下的后缀任意操作后最后得到的无论是\(0\)还是\(1\)在最后一次操作后都只能得到\(1\),故一定有解

因此上面的DP数组的第二维也只用开\(0/1/2\)即可,总复杂度\(O(n)\)

#include<cstdio>
#include<cstring>
#define RI register int
#define CI const int&
using namespace std;
const int N=300005,mod=1e9+7;
int n,f[N][3][3],ans; char s[N];
inline void inc(int& x,CI y)
{
	if ((x+=y)>=mod) x-=mod;
}
int main()
{
	RI i,j,k; scanf("%s",s+1); n=strlen(s+1); f[0][0][0]=1;
	for (i=0;i<n;++i)
	{
		for (j=0;j<=2;++j) 
		{
			if (s[i+1]=='0'||s[i+1]=='?')
			{
				for (k=0;k<=1;++k) inc(f[i+1][j][k+1],f[i][j][k]);
				inc(f[i+1][j][1],f[i][j][2]);
			}
			if (s[i+1]=='1'||s[i+1]=='?')
			{
				for (k=1;k<=2;++k) inc(f[i+1][j][k-1],f[i][j][k]);
				if (j<2) inc(f[i+1][j+1][0],f[i][j][0]);
			}			
		}
		if (s[i+1]=='1'||s[i+1]=='?') inc(f[i+1][2][0],f[i][2][0]);
	}
	for (i=0;i<=2;++i) for (j=0;j<=2;++j)
	if (i>=j) inc(ans,f[n][i][j]); return printf("%d",ans),0;
}

F - Checkers

神仙题,全是看题解的,完全不会的说……

我们首先注意到棋子的坐标很大,因此对于某个坐标\(x_i\),我们只需要知道它的系数

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

因此两个棋子合并的过程,其实可以看做将一个棋子的坐标\(\times 2\),另一个棋子坐标\(\times (-1)\),然后相加

那么显然\(x_i\)的系数一定是\((-1)^p\times 2^k\)的形式,显然确认了系数中\(2^k\)\(-2^k\)的个数即可统计答案,就是一个可重全排列

我们考虑当两个棋子合并时,我们可以新建一个点,从新建点向两个点分别连一条\(2\)和一条\(-1\)的边

最后会得到一棵树,每个叶节点的权值就是它到根的路径上的边权之积,显然我们只要统计这些叶节点的权值即可

但是考虑到不同的树对应的系数可能相同,因此我们换一个角度思考,从根开始考虑,每个节点可以向下伸出一条\(2\)\(-1\)的边

这样每次可以把一个\((-1)^p\times 2^k\)分裂成一个\((-1)^p\times 2^{k+1}\)和一个\((-1)^{p+1}\times 2^k\),直到分裂出\(n\)个叶节点为止

那么我们显然可以从\(2\)的幂次从小到大考虑,一开始有一个\(1\times 2^0\),设\(f_{i,j,k}\)表示当前已经有\(i\)个叶节点,有\(x\)个系数是负的,有\(y\)个系数是正的

注意到因为我们只需要知道\(2^k\)\(-2^k\)的数量,甚至不需要知道\(k\)具体是多少

我们考虑一次分裂的变化形如\(2^k\to (2^{k+1},-2^k),-2^k=(-2^{k+1},2^k)\),找一下规律就发现:

  • 每生成一个\(2^{k+1}\),就会用掉一个\(2^k\),并且回收一个\(-2^k\)
  • 每生成一个\(-2^{k+1}\),就会用掉一个\(-2^k\),并且回收一个\(2^k\)

于是我们只要枚举分裂\(a\)个负的,\(b\)个正的之后就可以转移:

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

直接大力做是\(O(n^5)\)的,我们发现这个转移的系数其实就和\(a-b\)有关,因此我们改为枚举\(a,b\)差值就可以把复杂度优化为\(O(n^4)\)

最后注意一下当从根节点开始做时需要考虑具体形态,因此转移系数要有所限制,最后答案就是\(f_{n,0}\times n!\)

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=50,mod=1e9+7;
int n,f[N+5][(N<<1)+5],fact[N+5],inv[N+5];
inline int quick_pow(int x,int p=mod-2,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline void init(CI n)
{
	RI i; for (fact[0]=i=1;i<=n;++i) fact[i]=1LL*fact[i-1]*i%mod;
	for (inv[n]=quick_pow(fact[n]),i=n-1;~i;--i) inv[i]=1LL*inv[i+1]*(i+1)%mod;
}
int main()
{
	RI i,j,k,d; for (scanf("%d",&n),init(n),f[0][N+1]=1,i=0;i<n;++i)
	{
		if (!i)
		{
			for (j=1,k=0,d=-k;d<=j;++d)
			(f[i+j+k][N+d]+=1LL*f[i][N+j-k]*inv[j-d]%mod*inv[k+d]%mod)%=mod; continue; 
		}
		for (j=0;j<=n-i;++j) for (k=0;k<=n-i-j;++k) if (j||k)
		for (d=-k;d<=j;++d) (f[i+j+k][N+d]+=1LL*f[i][N+j-k]*inv[j-d]%mod*inv[k+d]%mod)%=mod;
	}
	return printf("%d",1LL*f[n][N]*fact[n]%mod),0;
}

Postscript

我什么都不会弱的要死

posted @ 2020-10-30 20:04  空気力学の詩  阅读(271)  评论(0编辑  收藏  举报