把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【AtCoder】AtCoder Grand Contest 043 解题报告(E,F或许将成为永远的坑?)

点此进入比赛

\(A\):Range Flip Find Route(点此看题面

大致题意: 给定一个\(n\times m\)的黑白矩阵,每次操作你可以把一个矩形内所有格子颜色取反。问至少需要多少次操作才能使得从\((1,1)\)\((n,m)\)存在一条只往下或往右走的全由白色格子组成的路径。

动态规划

显然,我们可以发现,这操作等价于我们可以对连续行走的一段路颜色取反(这可以自己画图理解一下)。

因此我们设\(f_{i,j,0/1}\)表示当前走到\((i,j)\),是否正在取反。考虑转移方程:

  • 对于白色格子,\(f_{i,j,0}=min\{f_{i-1,j,0},f_{i,j-1,0},f_{i-1,j,1},f_{i,j-1,1}\},f_{i,j,1}=INF\)
  • 对于黑色格子,\(f_{i,j,0}=INF,f_{i,j,1}=min\{f_{i-1,j,0}+1,f_{i,j-1,0}+1,f_{i-1,j,1},f_{i,j-1,1}\}\)

于是这道签到题就这样做完了。

代码

#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 100
#define INF 1e9
using namespace std;
int n,m,f[N+5][N+5][2];char s[N+5][N+5];
int main()
{
	RI i,j;for(scanf("%d%d",&n,&m),i=1;i<=n;++i) scanf("%s",s[i]+1);
	s[1][1]^'#'?(f[1][1][0]=0,f[1][1][1]=INF):(f[1][1][0]=INF,f[1][1][1]=1);//初始化第一格
	for(i=1;i<=n;++i) f[i][0][0]=f[i][0][1]=INF;//把边界赋为INF
	for(i=1;i<=m;++i) f[0][i][0]=f[0][i][1]=INF;
	for(i=1;i<=n;++i) for(j=1;j<=m;++j) if(i^1||j^1)
		s[i][j]^'#'?(f[i][j][0]=min(min(f[i-1][j][0],f[i][j-1][0]),min(f[i-1][j][1],f[i][j-1][1])),f[i][j][1]=INF)//对于白色格子
		:(f[i][j][0]=INF,f[i][j][1]=min(min(f[i-1][j][0],f[i][j-1][0])+1,min(f[i-1][j][1],f[i][j-1][1])));//对于黑色格子
	return printf("%d",min(f[n][m][0],f[n][m][1])),0;//输出答案
}

\(B\):123 Triangle(点此看题面

大致题意: 给定一个由\(1,2,3\)组成的数组\(a_{1\sim n}\),定义\(x_{1,j}=a_j,x_{i,j}=|x_{i-1,j}-x_{i-1,j+1}|(1\le j\le n-i+1)\),求\(x_{n,1}\)

大致思路

这道题是闪指导在厕所里被灯闪了一下之后秒掉的题\(\%\%\%\)

考虑我们先求出\(x_2\)这个数组,然后就会发现,\(x_2\)中不可能存在\(3\),而最终答案中也不可能存在\(3\)

也就是说,接下来只有可能有\(0,1,2\)

对于只有\(0,2\)的情况,此时我们可以把\(2\)看作\(1\),转化为\(0,1\)的情况,最后答案乘\(2\)即可。

对于同时有\(0,1,2\)的情况,此时答案绝对不可能为\(2\),则我们发现这样一来\(0\)\(2\)就是等价的了,可以把\(2\)看作\(0\),同样转化为\(0,1\)的情况。

于是,我们只要考虑\(0,1\)的情况就可以了,则此时相减取绝对值实际上就是异或操作。

我们令原本的\(n\)\(1\)(因为我们处理的是\(x_2\)这个数组,而这个数组长度为\(n-1\)),然后就会发现第\(i\)个数(\(0\le i<n\))对答案的贡献次数实际上就是\(C_{n-1}^{i}\)

而既然是异或我们只要考虑其奇偶性,即模\(2\)意义下的值就可以了。

注意到由于模数是\(2\),我们不能直接求出组合数。因此我们用卢卡斯定理,这有一个常见的套路。

因为:

\[C_n^m=C_{n\ div\ 2}^{m\ div\ 2}\times C_{n\ mod\ 2}^{m\ mod\ 2} \]

所以递归下去,就相当于把\(n,m\)都转化为了二进制数。

而若二进制下存在某一位使得\(n\)这一位上为\(0\)\(m\)这一位上为\(1\),则最终的答案就为\(0\),否则为\(1\)

换言之,也就是二进制下\(n\)的每一位都要大于\(m\)的对应位,而用式子表示就是\(n\ xor\ m=n-m\)

代入到这题就是\((n-1)\ xor\ i=(n-1)-i\)

然后这道题就做完了。

代码

#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 1000000
using namespace std;
int n,a[N+5];char s[N+5];
int main()
{
	RI i;for(scanf("%d%s",&n,s),--n,i=0;i^n;++i) a[i]=abs(s[i]-s[i+1]);//将原先n减1,计算出x[2]
	RI f=1;for(i=0;i^n;++i) if(a[i]==1) {f=0;break;}//f=0表示存在1,f=1表示没有1,注意这题刚好能巧妙利用f的值
	for(i=0;i^n;++i) a[i]==2&&(a[i]=f);//存在1时把2视作0,没有1时把2视作1
	RI ans=0;for(i=0;i^n;++i) (((n-1)^i)==(n-1)-i)&&(ans^=a[i]);//根据组合数奇偶性判断这一位上数的贡献
	return printf("%d",ans*(f+1)),0;//存在1时答案乘1不变,不存在1时答案要乘上2
}

\(C\):Giant Graph(点此看题面

大致题意: 有三张\(n\)个点的图,分别有\(m_1,m_2,m_3\)条边。现在构造一张新图,每个点的编号用一个三元组\((x,y,z)\)表示,分别代表三张图中的点,且该点点权为\(10^{18(x+y+z)}\)。若\(x\)\(u\)之间有边,则\((x,y,z)\)\((u,y,z)\)之间有边,\(y,z\)两维同理。求新图的最大独立集的点权和。

贪心

这个\(10^{18(x+y+z)}\)摆在这里乍一看很吓人,但实际上却让这道题简单了许多。

为什么呢?我们发现,若\(x'+y'+z'<x+y+z\),那么即便选择\(n^3\)\(x'+y'+z'\)(尽管这当然是不可能的),它们的点权和也比不上选择一个\(x+y+z\)

于是就自然而然地发现,我们每次应尽量选择\(x+y+z\)较大的点。

而且,由于连边规则要求有两维一样,因此有边相连的两个点\(x+y+z\)的值不可能相等,即同一级别的点是互不影响的。

综上所述,这道题其实是道贪心题。

博弈论

什么?这道题居然会是博弈论?

考虑到\((n,n,n)\)显然是必选的,因此我们把它设为博弈的终止态。

如果我们把这个问题看作有一个棋子,每次可以把它移向值更大的点,不能再移动就输了。那么这就相当于是一个由三堆石子构成的\(Nim\)问题。也就是说,三张图之间相互独立,我们可以分别讨论。

由于标定了边的方向,原图变成了一张\(DAG\),而先手和后手轮流选择就恰好是一个独立集的过程。

因此,对于一个点\((x,y,z)\)\(SG1_x\ xor\ SG2_y\ xor\ SG3_z=0\),那么该点就可以被选择。

而求答案的时候只要枚举前两张图的\(SG\)\(i\)\(j\),统计下每一张图中\(SG=k\)\(10^{18i}\)的和\(tot_k\),那么就可以给答案加上:\(tot1_{i}\times tot2_{j}\times tot3_{i\ xor\ j}\)

代码

#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 100000
#define X 998244353
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,pw[N+5];
class FastIO
{
	private:
		#define FS 100000
		#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
		#define D isdigit(c=tc())
		char c,*A,*B,FI[FS];
	public:
		I FastIO() {A=B=FI;}
		Tp I void read(Ty& x) {x=0;W(!D);W(x=(x<<3)+(x<<1)+(c&15),D);}
}F;
class Graph//处理一张图的答案
{
	private:
		#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
		int m,ee,lnk[N+5],SG[N+5],vis[N+5];struct edge {int to,nxt;}e[2*N+5];
		I void dfs(CI x)//搜索求出SG
		{
			RI i;for(i=lnk[x];i;i=e[i].nxt) x<e[i].to&&!~SG[e[i].to]&&(dfs(e[i].to),0);//处理后继状态的SG值
			for(i=lnk[x];i;i=e[i].nxt) x<e[i].to&&(vis[SG[e[i].to]]=x);//标记哪些SG值出现过
			for(SG[x]=0;vis[SG[x]]==x;++SG[x]);//求出mex
		}
	public:
		int Mx,tot[N+5];
		I void Init()
		{
			RI i,x,y;for(F.read(m),i=1;i<=m;++i) F.read(x),F.read(y),add(x,y),add(y,x);//连边
			for(i=1;i<=n;++i) SG[i]=-1;for(i=1;i<=n;++i)//初始化SG值为-1表示未求解过
				!~SG[i]&&(dfs(i),0),Mx<SG[i]&&(Mx=SG[i]),Inc(tot[SG[i]],pw[i]);//统计信息
		}
}G1,G2,G3;
int main()
{
	RI i,j,ans=0;F.read(n);
	for(pw[0]=1,pw[1]=(long long)1e18%X,i=2;i<=n;++i) pw[i]=1LL*pw[i-1]*pw[1]%X;//预处理幂
	for(G1.Init(),G2.Init(),G3.Init(),i=0;i<=G1.Mx;++i)
		for(j=0;j<=G2.Mx;++j) ans=(1LL*G1.tot[i]*G2.tot[j]%X*G3.tot[i^j]+ans)%X;//统计答案
	return printf("%d",ans),0;
}

\(D\):Merge Triplets(点此看题面

大致题意:\(3n\)个数,你需要把它们分成\(n\)个有序三元组。每次选出每组第一个数中最小的那一个,取出并放入生成序列。求最终生成序列可能的情况数。

结论一

我们发现,由于每次取出最小的数,所以若一个数之后有若干比它小的数(注意是比它小,而不是递减),那么它们都应该属于一个三元组中。

也就是说,一个数之后最多只能有两个数比它小,且它们必然属于一个集合(这里的集合有别于题目中的三元组,大小可以为\(1/2/3\))。

再考虑下一个比它大的数必然是另一集合的开头的数,进而我们发现一个推论:每一个集合开头的数必然大于先前出现过的所有数

结论二

结论一实际上只能使得每个集合元素个数小于等于\(3\),却不能满足这些集合恰好能拼成\(n\)个三元组。

考虑对于大小为\(3\)的集合,它必然是一个三元组;大小为\(1\)的集合,既可以由大小为\(1,1,1\)的三个集合拼成一个三元组,也可以由大小为\(1,2\)的两个集合拼成一个三元组。

而对于大小为\(2\)的集合,只能与大小为\(1\)的集合共同拼成一个三元组。所以我们就得到了结论二:大小为\(2\)的集合个数小于等于大小为\(1\)的集合个数

显然有了这样两个结论,就已经保证了充要性,那么我们就可以\(DP\)生成序列的方案数了。

关于转移系数

我们设\(f_{i,j}\)表示已经确定了前\(i\)个数,大小为\(1\)的集合个数减去大小为\(2\)的集合个数为\(j\)\(j\)可能为负,因此我们给它加上\(3n\)的方案数。

然后发现这道题似乎用刷表法写会更容易。

一开始\(naive\)地去枚举新集合开头的数,也因此要记录下当前集合开头的数,复杂度是\(O(n^4)\)的。(可以用前缀和优化到\(O(n^3)\),但反正都过不去,懒得写了。。。)

于是被这个思路带偏,然后就走到死胡同里去了。。。

然而实际上,我们考虑只要维护出所有数的相对顺序,而确定\(n\)个数的相对顺序自然就确定了整个序列,也就不需要去枚举具体填什么值了。

假设当前确定了前\(i\)个数,然后加入新的一个集合,有三种情况:

  • 新加入的集合大小为\(1\):根据结论一的推论,这个数必然大于先前所有数,相对大小唯一,转移系数也就是\(1\)
  • 新加入的集合大小为\(2\):第\(i+2\)个数,肯定小于第\(i+1\)个数,而与已确定的\(i\)个数相比有\(i+1\)种相对大小关系(\(i\)个数有\(i+1\)个空隙),转移系数为\(i+1\)
  • 新加入的集合大小为\(3\):第\(i+3\)个数,与已确定的\(i\)个数和刚加入的第\(i+2\)个数相比有\(i+2\)种相对大小关系,转移系数在\(i+1\)的基础上再乘一个\(i+2\)

具体实现详见代码。

代码

#pragma GCC optimize(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 6000
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
using namespace std;
int n,X,f[N+5][2*N+5];
int main()
{
	RI i,j,k,x;scanf("%d%d",&n,&X),n*=3,f[0][n]=1;//这里先将n乘3,方便后续操作
	for(i=0;i^n;++i) for(j=0;j<=2*n;++j)
		i+1<=n&&Inc(f[i+1][j+1],f[i][j]),//加入一个数
		i+2<=n&&(f[i+2][j-1]=(1LL*(i+1)*f[i][j]+f[i+2][j-1])%X),//加入两个数
		i+3<=n&&(f[i+3][j]=((1LL*(i+1)*(i+2))%X*f[i][j]+f[i+3][j])%X);//加入三个数
	RI ans=0;for(j=n;j<=2*n;++j) Inc(ans,f[n][j]);return printf("%d",ans),0;//统计并输出答案
}
posted @ 2020-05-18 11:14  TheLostWeak  阅读(210)  评论(0编辑  收藏  举报