Problem
Input
Output
Hint
Solution
首先感喟一下,今天又是让我们看着一坨图片消耗脑细胞,所以OJ多没诚意,抄一下都不肯,也不知道用微信把图片转成文字。
终结了我的胡思乱想,我发现这题是我几乎从未接触过的博弈,感觉可能切不了。此时右边的ZJQ说这题不需要用什么鬼sg函数,只是一道普通的博弈,想个几十分钟,或者不用,想个十几分钟就能想出来。我信了,于是在那里猛想,但是这道题的防御依旧固若金汤。
你可以想到,最终我还是没想出来,并且连暴力都没想出来,比赛时索性没交这题。
下面是解题思路。
首先,设f[a][b]表示a为大小为1的堆的数目,b为其余堆的合并后再取完的步数(即其余堆的大小和+数目-1)时先手是否必胜。至于为什么要这么设状态,因为这样可以把许多冗余状态合并在一起,优化时间复杂度;而至于为什么这样设状态是对的,就是说,有无可能有两种局面恰好均满足上述状态,但一个是必胜,一个是必败,我只能说,我也不知道该怎么证明,我们可以分类讨论一下所有堆大小均大于1的和有若干堆大小为1的情况。
对于前者,我们可以再分类讨论一下b的奇偶性。若b为奇数,则先手必胜;否则必败。这个证明很简单,因为如果b为奇数,则先手可以一直在合并,这样的话会让b变为偶数;而后手有两种选择:1.吃掉一个;2.跟着先手合并。不管他怎么选,都会将b从偶数变成一个奇数,因为他选1的话,由于所有堆的大小均大于1,吃了一个不能减少堆数,我们则可以趁机将那个被他吃掉一个的堆合并掉,这样就可以保证从整个局面看,轮到后手时,始终不会出现大小为1的堆,也就不会给后手翻盘的机会。所以b为奇数,先手必胜。而如果b为偶数,先手不管怎么搞都会把它变成奇数,则后手进入必胜局面。
对于后者,则没有那么简单了。
首先,我们考虑f[a][b]可以从那几个状态推过来。
显然由下面四个状态转移而来:
1. f[a-2][b+2+(b?1:0)](把两个为1的堆合并,所以a减2,b由于大小增加加了个2,而如果原本b中无堆,则不必合并;否则我们须花费1步,把新建的这个大小为2的堆合并到其他地方)
2. f[a-1][b](取走某个大小为1的堆)
3. f[a-1][b+1](将某个大小为1的堆与某个大小大于1的堆合并)
4. f[a][b-1](将某个大小大于1的堆吃掉一个石子或者合并两个这样的堆)
需要满足的条件显然。
至于边界条件,显然为f[0][b]=b%2。
注意,如果我们不断地做4操作,可能会将b退化为大小为1的堆,所以当b==1时,b中肯定只剩下了一个堆,且此堆的大小肯定为1,于是我们把这个局面变为f[a+1][0]。
对了,还有一点很重要,就是我们在每次做完记忆化搜索的时候(或开始做之前),我们不必讲记忆化的内容清空,因为它的读入并不影响每个局面胜败与否。这样的话,我们就可以藐视多组数据了。
时间复杂度:
O(T+N2ai)
O
(
T
+
N
2
a
i
)
。
Code
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#define N 51
#define A 1001
#define fo(i,a,b) for(i=a;i<=b;i++)
int T,t,i,n,a,b,x,f[N][N*A];
int dfs(int a,int b)
{
if(f[a][b]>=0)return f[a][b];
if(!a)return f[a][b]=b%2;
if(b==1)return f[a][b]=dfs(a+1,0);
int t=1;
if(a>1)t=min(t,dfs(a-2,b+2+(b?1:0)));
if(a)t=min(t,dfs(a-1,b));
if(a&&b)t=min(t,dfs(a-1,b+1));
if(b)t=min(t,dfs(a,b-1));
return f[a][b]=!t;
}
int main()
{
scanf("%d",&T);
memset(f,-1,sizeof f);
fo(t,1,T)
{
scanf("%d",&n);
a=b=0;
fo(i,1,n)
{
scanf("%d",&x);
if(x==1)
a++;
else b+=x+1;
}
b-=(bool)b;
printf(dfs(a,b)?"YES\n":"NO\n");
}
}