【AtCoder】AtCoder Grand Contest 016 解题报告

点此进入比赛

A:Shrinking(点此看题面

  • 给定一个长度为(n)的字符串。
  • 每次操作得到一个长度减(1)的字符串,其中第(i)个字符可以任选原串第(i)或第(i+1)个字符。
  • 问至少几次操作能使得到的字符串只有一种字符。
  • (nle100)

签到题

考虑枚举最后的字符。

对于每个位置找到它右侧第一个该字符,则当前位置到该字符的距离就是使当前位置变成该字符的最小操作次数。

代码:(O(26n))

#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
using namespace std;
int n,pre[N+5],nxt[N+5];char s[N+5]; 
int main()
{
	RI i,j,t,nxt,ans=1e9;for(scanf("%s",s+1),n=strlen(s+1),i='a';i<='z';++i)//枚举最后的字符
		{for(t=0,nxt=n+1,j=n;j>=1;--j) s[j]==i&&(nxt=j),t=max(t,nxt-j);ans=min(ans,t);}//找到右侧第一个字符
	return printf("%d
",ans),0;
}

B:Colorful Hats(点此看题面

  • (n)个人,每个人头上有一顶帽子。
  • 给出每个人看到的帽子颜色种数,问是否可能。
  • (nle10^5)

分类讨论

显然,每个人看到的帽子颜色种数极差必然小于等于(1)(因为每个人最多少看到一种颜色)。

考虑看到帽子颜色种数等于最小值的人(设人数为(t0))必然是自己的帽子独自一种颜色。

而等于最大值的人(设人数为(t1)),颜色种数最少时是所有人同色,最多时是两两成对。

即需要满足颜色总数(即最大值)在([t0+1,t0+lfloorfrac{t1}2 floor])的区间内。

但注意还要特判最小值等于最大值,这时候可能是所有人都独自一种颜色((n-1)),也可能存在颜色相同(([1,lfloorfrac n2 floor]))。

代码:(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 100000 
using namespace std;
int n,a[N+5];
int main()
{
	RI i,Mn=N,Mx=0;for(scanf("%d",&n),i=1;i<=n;++i)
		scanf("%d",a+i),Mn=min(Mn,a[i]),Mx=max(Mx,a[i]);if(Mx-Mn>1) return puts("No"),0;//统计最大值、最小值,判极差>1
	if(Mn==Mx) return puts(Mn==n-1||Mn<=n/2?"Yes":"No"),0;//如果最小值等于最大值,有两种可能
	RI t0=0,t1=0;for(i=1;i<=n;++i) Mn==a[i]&&++t0,Mx==a[i]&&++t1;//统计最小值人数和最大值人数
	return puts(t0+1<=Mx&&Mx<=t0+(t1/2)?"Yes":"No"),0;//判断颜色种数是否在合法区间内
}

C:+/- Rectangle(点此看题面

  • 给定(h,w),要求构造一个(H imes W)的矩阵,满足:
    • 值域为([-10^9,10^9])
    • 整个矩阵元素之和为正数。
    • 每个(h imes w)的子矩阵元素之和为负数。
  • (H,Wle500)

构造

考虑先给所有元素填上(v=lfloorfrac{10^9}{h imes w} floor)(这差不多是理论上能取的最大值)。

然后我们枚举每一个(h imes w)的子矩阵,对于不符合条件的子矩阵在它的右下角放一个(-v imes(h imes w-1)-1),使得这个矩阵之和为负数。

发现其实一个子矩阵是否合法就在于有没有放过这个大负数,可以直接二维前缀和得到某个子矩阵是否放过。

最后判一下整个矩阵和是否为正数即可。

代码:(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 500
#define LL long long
using namespace std;
int n,m,x,y,a[N+5][N+5],s[N+5][N+5];
int main()
{
	#define Check() (s[i][j]-s[i-x][j]-s[i][j-y]+s[i-x][j-y])//检查是否放过
	RI i,j,v;for(scanf("%d%d%d%d",&n,&m,&x,&y),v=1e9/x/y,i=1;i<=n;++i) for(j=1;j<=m;++j) a[i][j]=v;//初始都赋值为v
	LL t=1LL*n*m*v;for(i=x;i<=n;++i) for(j=y;j<=m;++j)//枚举每个子矩阵
		s[i][j]=s[i-1][j]+s[i][j-1]-s[i-1][j-1],!Check()&&(t+=(a[i][j]=-v*(x*y-1)-1)-v,++s[i][j]);//不符合条件的在右下角放个大负数
	if(t<=0) return puts("No"),0;for(puts("Yes"),//判整个矩阵元素和是否为正
		i=1;i<=n;++i) for(j=1;j<=m;++j) printf("%d%c",a[i][j]," 
"[j==m]);return 0;
}

D:XOR Replace(点此看题面

  • 给定一个长度为(n)的序列(a)
  • 每次操作可以把一个(a_i)修改为当前整个序列的异或和。
  • 问至少多少次操作能得到给定序列(b)
  • (nle10^5)

基础结论

我们发现,对一个(a_i)操作之后,(a_i)就变成了了整个序列原先的异或和,而整个序列的异或和就变成了原先的(a_i)

所以我们可以在(a)的末尾加上一个(a_{n+1}=igoplus_{i=1}^na_i),然后问题就变成了至少通过几次将一个元素和(a_{n+1})交换的操作能够得到(b)

推性质

首先,如果(a_i=b_i),那么我们肯定不会去动它,可以直接忽略(不算在后面的点数中)。

否则,先假设(a_i)各不相同,那么根据目标位置连边可以得到若干置换环,容易发现答案就是点数+置换环个数-1

对于这个式子的理解,就是每个点至少需要操作一次,而对于每个置换环我们需要先花一次代价进入这个环,至于减(1)是因为(a_{n+1})原本就处在一个连通块中。

但是,(a_i)不一定各不相同,连边需要向所有值相同的位置连边,可能连出花来。

显然我们没必要对于位置连边,其实完全可以直接针对于值连边。

然后稍微手玩几个小数据就会发现,上面的结论其实依然适用,只不过变成了点数+连通块个数-1

而维护连通块众所周知只需要一个并查集就可以了。

代码:(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 100000
using namespace std;
int n,a[N+5],b[N+5],c[N+5],dc,dv[N+5],fa[N+5];
I int getfa(CI x) {return fa[x]?fa[x]=getfa(fa[x]):x;}
int main()
{
	RI i;for(scanf("%d",&n),i=1;i<=n;++i) scanf("%d",a+i),a[n+1]^=a[i];//求出a[n+1]
	for(dv[dc=1]=a[n+1],i=1;i<=n;++i) scanf("%d",b+i),a[i]^b[i]&&(dv[++dc]=a[i]);//离散化
	#define G(x) lower_bound(dv+1,dv+dc+1,x)-dv
	for(sort(dv+1,dv+dc+1),dc=unique(dv+1,dv+dc+1)-dv-1,i=1;i<=n;++i) if(a[i]^b[i])//忽略a[i]=b[i]的点
		{if(dv[G(b[i])]^b[i]) return puts("-1"),0;else ++c[a[i]=G(a[i])],--c[b[i]=G(b[i])];}//统计每个数出现次数
	for(++c[a[n+1]=G(a[n+1])],b[n+1]=-1,i=1;i<=dc;++i) if(c[i])
		{if(c[i]==1&&!~b[n+1]) b[n+1]=i;else return puts("-1"),0;}//找出b[n+1],同时判无解
	RI x,y,t=0;for(i=1;i<=n;++i) a[i]^b[i]&&++t;//点数
	for(i=1;i<=n+1;++i) a[i]^b[i]&&(x=getfa(a[i]))^(y=getfa(b[i]))&&(fa[x]=y);//并查集
	for(i=1;i<=dc;++i) t+=getfa(i)==i;return printf("%d
",t-1),0;//点数+连通块数-1
}

E:Poor Turkeys(点此看题面

  • (n)只火鸡,依次进行(m)次操作。
  • 每次操作给定两个数(x,y),表示杀掉两只火鸡中的一只(如果只剩一只则必杀)。
  • 问有多少对火鸡最后可能都存活。
  • (nle400,mle10^5)

动态规划

发现我居然一年前在一次模拟赛中就做掉了这道题?

考虑设(f_{i,j})表示(i)(j)是否可能都存活。((f_{x,y}=1)表示不能)

对于一次操作(x,y),如果(x)存活,则(y)在此之前必须存活。

也就是说,如果(f_{y,z}=1),那么(f_{x,z}=1),因为(z)不死则(y)之前就必死,那么(x)此时也必死。

就这样搞一遍就好了?

代码:(O(nm))

#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 400
#define M 100000
using namespace std;
int n,m,f[N+5][N+5];
int main()
{
	RI i,j,x,y;for(scanf("%d%d",&n,&m),i=1;i<=m;++i)//枚举操作
	{
		#define D(x,y) (f[x][y]=f[y][x]=1)//x,y不能同时存活,双向标记
		for(scanf("%d%d",&x,&y),j=1;j<=n;++j) f[y][j]&&D(x,j),f[x][j]&&D(y,j);//要一只火鸡存活,则在此之前另一只火鸡不能死
		f[x][x]&&(f[y][y]=1),f[y][y]&&(f[x][x]=1),D(x,y);//如果一只火鸡已经必死,则另一只火鸡也必死
		if(f[x][x]) for(j=1;j<=n;++j) D(x,j);if(f[y][y]) for(j=1;j<=n;++j) D(y,j);//如果一只火鸡必死,那么不可能和其他火鸡共同存活
	}
	RI t=0;for(i=1;i<=n;++i) for(j=i+1;j<=n;++j) t+=!f[i][j];return printf("%d
",t),0;//枚举火鸡对算答案
}

F:Games on DAG(点此看题面

  • 一张(n)个点的有向图,两颗石子分别在(1)号点和(2)号点。
  • 每次可以将一个石子沿一条边移动,谁不能移动就输了。
  • 求有多少边集的子集,满足先手必胜。
  • (nle15)

正难则反

先手必胜意味着(SG(1)oplus SG(2) ot=0),即(SG(1) ot=SG(2))

正难则反,我们求出(SG(1)=SG(2))的方案数,然后用总方案数减去即可。

动态规划

虽然是问边集的子集,但边的数量过大,我们仍考虑记录点集的子集。

(f_S)表示点集(S)满足(SG(1)=SG(2))时的连边方案,显然(1,2)要么都在(S)中,要么都不在(S)中。

显然一个点集可以分成必胜点和必败点两部分,于是我们枚举必败点子集,发现边可以分四类:

  • 必败点( ightarrow)必败点:显然不可能,因为能到必败点的点必胜。
  • 必败点( ightarrow)必胜点:随意连边。
  • 必胜点( ightarrow)必败点:至少要有一条边,只要用全部连边方案减(1)(不连边的情况)即可。
  • 必胜点( ightarrow)必胜点:随意连边,即(f(T))(T)为必胜点集)。

于是转移方程就很简单了。

代码:(O(3^nn))

#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 15 
#define X 1000000007
using namespace std;
int n,m,nxt[N+5],f[1<<N],g[1<<N];
int main()
{
	RI i,x,y;for(scanf("%d%d",&n,&m),i=1;i<=m;++i) scanf("%d%d",&x,&y),nxt[x]|=1<<y-1;//状压连边状态
	#define Check(x) ((x&1)==((x>>1)&1))//检验1,2是否在一起
	RI j,k,t,l=1<<n;for(f[0]=i=1;i^l;++i) if(g[i]=g[i>>1]+(i&1),Check(i))//枚举点集DP
		for(j=i;j;f[i]=(1LL*f[i^j]*t+f[i])%X,j=(j-1)&i) if(Check(j)) for(t=k=1;k<=n;++k)//枚举必败点集
			j>>k-1&1&&(t=1LL*t*(1<<g[nxt[k]&(i^j)])%X),(i^j)>>k-1&1&&(t=1LL*t*((1<<g[nxt[k]&j])-1)%X);//统计两点集间的连边方案数
	for(t=i=1;i<=m;++i) (t<<=1)%=X;return printf("%d
",(t-f[l-1]+X)%X),0;//用总方案数减非法方案数
}
原文地址:https://www.cnblogs.com/chenxiaoran666/p/AtCoderAGC016.html