AtCoder Grand Contest 026

Preface

ABCD还算简单,EF不会,但说实话E当时没仔细想其实不是很难,F确实挺麻烦的


A - Colorful Slimes 2

显然修改涂成未出现过的颜色是最优的,找一下连续同色段即可

#include<cstdio>
#define RI register int
#define CI const int&
using namespace std;
const int N=105;
int n,a[N],lst,ans;
int main()
{
	RI i; for (scanf("%d",&n),i=1;i<=n;++i)
	scanf("%d",&a[i]); for (i=1;i<=n+1;++i)
	if (a[i]!=a[i-1]) ans+=(i-lst>>1),lst=i;
	return printf("%d",ans),0;
}

B - rng_10s

小清新数学题。考虑先判掉一些显然的情况,很容易看出当\(B>A\)\(B>D\)时无解,而当\(C\ge B\)\(D\ge B\)时必定有解,现在只要考虑\(D<B\)的情况即可

\(D<B\)时我们发现若当前个数大于\(C\%B\)就不合法了,因为此时再进行一次拿走\(B\)的操作就会不够了

容易发现我们假设进行了\(k\)次加\(D\)的操作,上面的情况就变成是否存在某个\(k\)满足\(A-k\times D>C\)在膜\(B\)意义下

考虑最大化左边的值(注意不能直接移项过去),结合剩余系的相关知识可以推出左边的最大值为\(A\%B+\lfloor \frac{(B-A\%B-1)}{\gcd(B,D)}\rfloor \times \gcd(B,D)\)

#include<cstdio>
#include<algorithm>
#define RI register int
#define CI const int&
using namespace std;
int t; long long A,B,C,D;
int main()
{
	for (scanf("%d",&t);t;--t)
	{
		scanf("%lld%lld%lld%lld",&A,&B,&C,&D);
		if (B>A||B>D) { puts("No"); continue; }
		if (C>=B&&D>=B) { puts("Yes"); continue; }
		puts(A%B+(B-A%B-1)/__gcd(B,D)*__gcd(B,D)>C%B?"No":"Yes");
	}
	return 0;
}

C - String Coloring

刚开始脑抽了把串反过来搞,结果这样每一位都不对应了白写了好久

后来发现直接无脑上一个meet in middle即可,把状态写个Hash扔到map里即可

#include<cstdio>
#include<iostream>
#include<map>
#define RI register int
#define CI const int&
#define mp make_pair
using namespace std;
const int N=20;
typedef unsigned long long ull;
struct Hasher
{
	ull x,y;
	inline Hasher(const ull& X=0,const ull& Y=0) { x=X; y=Y; }
	friend inline bool operator < (const Hasher& A,const Hasher& B)
	{
		return A.x!=B.x?A.x<B.x:A.y<B.y;
	}
	friend inline Hasher operator + (const Hasher& A,const Hasher& B)
	{
		return Hasher(A.x+B.x,A.y+B.y);
	}
	friend inline Hasher operator * (const Hasher& A,const Hasher& B)
	{
		return Hasher(A.x*B.x,A.y*B.y);
	}
}; const Hasher seed(233,163);
map <pair<Hasher,Hasher>,int> f; long long ans; int n; char a[N<<1];
inline void DFS1(CI now,const Hasher& A=Hasher(),const Hasher& B=Hasher())
{
	if (now>n) return (void)(++f[mp(A,B)]);
	DFS1(now+1,A*seed+Hasher(a[now],a[now]),B); DFS1(now+1,A,B*seed+Hasher(a[now],a[now]));
}
inline void DFS2(CI now,const Hasher& A=Hasher(),const Hasher& B=Hasher())
{
	if (now==n) return (void)(ans+=f[mp(A,B)]);
	DFS2(now-1,A*seed+Hasher(a[now],a[now]),B); DFS2(now-1,A,B*seed+Hasher(a[now],a[now]));
}
int main()
{
	RI i,j,p,q; scanf("%d%s",&n,a+1);
	return DFS1(1),DFS2(n<<1),printf("%lld",ans),0;
}

D - Histogram Coloring

陈指导的\(O(n)\)确实可怕,想我这种蒟蒻只会写\(O(n^3)\)的说还写得又臭又长

首先我们考虑只在矩形上填会怎样,容易发现如果我们填好了某一行,下一行填这一行取反后的矩阵是必定合法的

而且我们发现当且仅当上一行没有相邻的同色格子时下一行可以和上一行填的完全相同,这样的情况显然对于第一行只有两种情况

因此我们可以考虑设计一个状态\(f(l,r,d)\)表示在\([l,r]\)且高度\(\ge d\)的部分中的含相邻的同色格子和不含相邻的同色格子的方案数分别是多少

转移的话显然可以根据\([l,r]\)中的最低的高度把整个部分切割,边界条件就是构成了矩形

然后这样就做完了,不给状态记忆化都是\(O(n^3)\)可以通过的

PS1:注意切割后上面没有格子的位置可以随便填,但是也要分情况记录到有无相邻的同色格子上

PS2:当矩形宽为\(1\)的时候快速幂会出现负数挂掉,因此对于\(n=1\)\(h_i\)大于两边的位置高出的部分要截掉再算

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=105,mod=1e9+7;
struct element
{
	int x,y; //x:Exist same color closed; y:Not Exist;
	inline element(CI X=0,CI Y=0) { x=X; y=Y; }
}; int n,h[N],ext=1;
inline int quick_pow(int x,int p,int mul=1)
{
	for (;p;p>>=1,x=1LL*x*x%mod) if (p&1) mul=1LL*mul*x%mod; return mul;
}
inline element DP(CI l,CI r,CI d)
{
	RI i; bool flag=1; for (i=l+1;i<=r&&flag;++i) if (h[i]!=h[i-1]) flag=0;
	if (flag) return element((quick_pow(2,r-l+1)-2+mod)%mod,quick_pow(2,h[l]-d-1));
	int c1=1,c2=1,sum=0,lst=0,mi=h[l]; element ret; //c1:exist same color closed; c2:Not Exist;
	for (i=l+1;i<=r;++i) mi=min(mi,h[i]); for (i=l;i<=r;++i)
	if (!lst&&h[i]>mi) lst=i; else if (lst&&h[i]==mi)
	{
		sum+=i-lst; element nxt=DP(lst,i-1,mi);
		c1=1LL*c1*(nxt.x+4LL*nxt.y%mod)%mod; c2=2LL*c2*nxt.y%mod; lst=0;
	}
	if (lst)
	{
		sum+=r+1-lst; element nxt=DP(lst,r,mi);
		c1=1LL*c1*(nxt.x+4LL*nxt.y%mod)%mod; c2=2LL*c2*nxt.y%mod;
	}
	ret.x=1LL*(c1-c2+mod)%mod*quick_pow(2,r-l+1-sum)%mod;
	(ret.x+=1LL*c2*(quick_pow(2,r-l+1-sum)-2+mod)%mod)%=mod;
	ret.y=1LL*c2*quick_pow(2,mi-d-1)%mod; return ret;
}
int main()
{
	RI i; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&h[i]);
	if (n==1) return printf("%d",quick_pow(2,h[1])),0;
	for (i=1;i<=n;++i) if (h[i]>h[i-1]&&h[i]>h[i+1])
	ext=1LL*ext*quick_pow(2,h[i]-max(h[i-1],h[i+1]))%mod,h[i]=max(h[i-1],h[i+1]);
	element ans=DP(1,n,0); return printf("%d",1LL*ext*(ans.x+2LL*ans.y)%mod),0;
}

E - Synchronized Subsequence

考虑令\(a=1,b=-1\),那么我们可以在所有前缀和为\(0\)的地方把序列划分开,容易发现每段之间互不影响

然后有一个结论:在每一段内,一对\(ab\)的位置\((x,y)\)的大小关系相同

考虑反证,若不相同那么中间必然出现一个前缀和为\(0\)的位置将它们分隔开

因此我们可以根据每一段的开头是\(a\)还是\(b\)分类讨论

  • 当开头为\(a\)时,若这个段后面有开头为\(b\)的段时这个段显然没有存在的意义可以直接删除,否则此时这个串的贡献肯定是对答案的一段后缀,因此我们找出最长的\(abab\cdots\)的形式是最优的
  • 当开头为\(b\)时,假设在这个段中第一个被选择的一对\(ab\)\((x,y)\),那么显然所有位置在\([y+1,x-1]\)中的\(b\)都可以选,然后这样又会选走后面的一些\(a\)。到最后就会把这一段的后缀全取了。因此我们直接暴枚所有的后缀即可

从后往前贪心即可,总体复杂度为\(O(n^2)\)

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=6005;
struct my_string
{
	int n; char s[N];
	inline my_string(CI N=0) { n=N; }
	inline char& operator [] (CI x) { return s[x]; }
	inline void add(const char& ch) { s[++n]=ch; }
	inline void print(void)
	{
		for (RI i=n;i;--i) putchar(s[i]);
	}
}ans; int n,m,l[N],r[N],pos[N][2],cur; char s[N]; bool flag;
inline bool cmp(my_string& A,my_string& B)
{
	for (RI i=1;i<=n;++i) if (A[A.n-i+1]!=B[B.n-i+1])
	return A[A.n-i+1]<B[B.n-i+1]; return A.n<B.n;
}
int main()
{
	RI i,j,k; for (scanf("%d%s",&n,s+1),i=1;i<=(n<<1);++i)
	{
		if (!cur) l[++m]=i; cur+=s[i]=='a'?1:-1; if (!cur) r[m]=i;
	}
	for (i=m;i;--i) if (s[l[i]]=='b')
	{
		flag=1; my_string ret=ans;
		for (j=r[i]-l[i]+1>>1;~j;--j)
		{
			int c1=0,c2=0; my_string ns=ans;
			for (k=r[i];k>=l[i];--k)
			if (s[k]=='a') { if (++c1<=j) ns.add('a'); }
			else { if (++c2<=j) ns.add('b'); }
			if (cmp(ret,ns)) ret=ns;
		}
		ans=ret;
	} else if (!flag)
	{
		int c1=0,c2=0; for (j=l[i];j<=r[i];++j)
		if (s[j]=='a') pos[++c1][0]=j; else pos[++c2][1]=j;
		for (j=1,k=0;j<=r[i]-l[i]+1>>1;++j)
		if (k<pos[j][0]) ans.add('b'),ans.add('a'),k=pos[j][1];
	}
	return ans.print(),0;
}

F - Manju Game

好劲的博弈题,想通了确实不难的说,但独立分析确实挺烦的

考虑设先手为\(A\),后手为\(B\),我们首先发现当\(A\)取走两个端点时两个人的操作就固定了,\(A\)必然可以取走所有和它第一次取的点奇偶性相同的点

然后考虑当\(A\)先取了一个中间点后的情况,容易发现此时左右两边就变成了两个子问题,此时

  • 若左右两边都是奇数,这种情况出现在为奇数时。那么无论\(B\)取哪一边,在这一边取完后\(A\)在另一边依然可以获得先手,这就和原来的情况等价
  • 若左右两边都是偶数,这种情况出现在\(n\)为奇数时。那么无论\(B\)取哪一边,在这一边取完后\(B\)可以在另一边获得先手。然后我们发现这是\(A\)第一步取的就是奇数位,但是这样转让了先手权给\(B\)显然不如直接取端点把所有奇数位选走,因此这种情况不做考虑
  • 若左右两边一奇一偶,这种情况出现在\(n\)为偶数时。那么\(B\)一定先操作偶数的那一边来获得另一边的先手权,然后取最优策略。此时\(A\)还不如直接取两个端点来避免这种情况,这样不仅取到了之前能取到的还使\(B\)没有得到先手权,一定不劣于前一种方案

因此我们发现当\(n\)为偶数时答案就是\(\max(\text{奇数位的和},\text{偶数位的和})\),考虑上面的\(n\)为奇数的第一种情况

那么此时\(A\)一定会取走中间的一个偶数位上的数,此时问题根据\(B\)的选边被划分开来

我们考虑把\(B\)的每次决策看做构成一棵二叉树,那么\(B\)一定会选择某些叶子节点来使得\(A\)的收益最小

我们先假设所有的偶数位都被\(A\)取走了,那么此时\(A\)在某个区间内取端点的收益就是这个区间的奇数位的和减去偶数位的和(因为此时两端点都是奇数)

我们现在就要找出一个叶子节点的集合,满足在这个叶子上进行取端点的收益的最小值尽量大

最小值最大显然考虑二分答案\(x\),那么现在问题变为我们是否能删除某些偶数位上的数来使得剩下的序列中任意一个极大的连续段之和都大于等于\(x\)

如果暴力DP的话需要枚举右端点的同时在枚举划分点,复杂度显然无法通过

考虑到这里我们的DP存储的信息是\(0/1\),可以考虑贪心,每次保留值为\(1\)的点中前缀和最小的即可

总复杂度\(O(n\log \sum a_i)\),足以通过此题

#include<cstdio>
#include<iostream>
#define RI register int
#define CI const int&
using namespace std;
const int N=300005;
int n,a[N],pfx[N],ans,sum;
inline bool check(CI x)
{
	int cur=0; for (RI i=1;i<n;i+=2) if (pfx[i]-cur>=x)
	cur=min(cur,pfx[i+1]); return pfx[n]-cur>=x;
}
int main()
{
	RI i; for (scanf("%d",&n),i=1;i<=n;++i) scanf("%d",&a[i]),sum+=a[i];
	if ((n&1)==0)
	{
		int s[2]={0,0}; for (i=1;i<=n;++i) s[i&1]+=a[i];
		return printf("%d %d",max(s[0],s[1]),min(s[0],s[1])),0;
	}
	for (i=1;i<=n;++i) pfx[i]=pfx[i-1]+(i&1?a[i]:-a[i]);
	int l=1,r=sum,mid; while (l<=r)
	if (check(mid=l+r>>1)) ans=mid,l=mid+1; else r=mid-1;
	for (i=1;i<=n;++i) if ((i&1)==0) ans+=a[i];
	return printf("%d %d",ans,sum-ans),0;
}

Postscript

这场题解写得真的长233,半停课生活开始了可以Rush一把了

posted @ 2020-10-26 11:48  空気力学の詩  阅读(118)  评论(0编辑  收藏  举报