【BZOJ1413】[ZJOI2009]取石子游戏(博弈论,动态规划)
【BZOJ1413】[ZJOI2009]取石子游戏(博弈论,动态规划)
题面
题解
神仙题.jpg。\(ZJOI\)是真的神仙。
发现\(SG\)函数等东西完全找不到规律,无奈只能翻题解。
首先设\(L[i][j]\)表示在\([i,j]\)这一段区间的左侧放上一堆数量为\(L[i][j]\)的石子后,先手必败。同理定义\(R[i][j]\)表示右侧。
首先我们可以证明\(L[i][j]\)唯一,假设存在两个\(L[i][j]\),显然较大的那个可以通过一步转移转移到较小的那个,所以不合法。因此\(L[i][j]\)唯一。
接下来考虑如何证明\(L[i][j]\)一定存在。假设\(L[i][j]\)不存在,那么对于这段区间而言,在左边加上任意一堆石子先手都必胜,既然先手必胜意味着先手进行一步操作之后可以到达一个必败态,这里分情况讨论。假设先手拿的是最左边的一堆石子,因为不存在\(L[i][j]\),所以只要拿了左边的石子之后,当前局面都是必胜态,所以不可能拿左边的石子。那么只能拿右边的石子,那么无论右边拿了一定量之后,无论左边添加了多少,都是一个必败态,那么此时后手在左侧随便拿走一定数量,这个状态也还是一个必败态,显然也不成立。因此\(L[i][j]\)必定存在。
综上,我们知道了\(L[i][j]\)一定存在并且唯一,而\(L[][],R[][]\)显然是对称的,因此\(R[i][j]\)也满足上述性质。
现在考虑如何求解\(L[i][j]\),\(R[i][j]\)同理。首先边界情况显然,\(L[i][i]=a[i]\),因为只剩下两堆一模一样的情况的时候,后手只需要模仿先手的行动对称执行就好了,这样子一定不会输,即先手必败。
接下来来大力分类讨论,为了方便(为了抄),设\(L=L[i][j-1],R=R[i][j-1],x=a[j]\)
- \(x=R\)
这种情况下显然只需要直接把\(a[j]\)放进去就好了,即这个区间本身就是一个必败态。所以\(L[i][j]=0\)。 - \(x<L,x<R\)
这种情况下\(L[i][j]=x\)。这种情况下最靠左的\(L[i][j]\)和\(x=a[j]\)是相同的,意味着先手无论怎么取,后手显然可以学着它的方法取,也就意味着左右两堆中显然必然会先拿完一堆,此时后手学着拿的那一堆的石子数一定也是小于\(L,R\)的。假设先手先拿完了最靠右的一堆,即剩下了\([i,j-1]\),因为\(L[i][j-1]\)表示的是在这一段区间最左侧加入一个\(L[i][j-1]\)的堆,无论先手怎么取先手都是必败的,那么我们等价的认为先手取走了这一堆的一部分,显然后手是必胜的。假如先手先取完的是最左的一堆,同理,\(R[i][j-1]\)的含义是在最右侧加入了一堆,而\(a[j]<R[i][j-1]\),我们还是可以等价的认为先手在这一堆中取走了若干石子,而这个状态对于先手而言是必败状态,因此显然后手必胜。 - \(R<x<L\)
这种情况下\(L[i][j]=x-1\)。这样子考虑,假设先手先拿了左边这一堆,那么假设还剩下了\(z\)个石子,如果\(z<R\),后手把右侧的那一堆也给拿成\(z\)就变成了上面的情况。如果\(z\ge R\),那么后手把最后那一堆拿成\(z+1\),于是又回到了这种情况,相当于这种情况递归处理。如果先手先拿的是右侧的这一堆,还是一样的,假设把它拿成了\(z\),如果\(z<R\),同上可以变成\(x<L,x<R\)的情况;如果\(y=R\),直接把左边拿完,就变成了\(R[i][j-1]\)的定义了,先手必败;如果\(z>R\),把左边那堆变成\(z-1\),同样递归处理。 - \(L<x<R\)
分析同上,\(L[i][j]=x+1\)。 - \(x>L,x>R\)
\(L[i][j]=x\)。还是一样的,假设先手把其中一堆拿成了\(z\)。如果\(z>L,R\),跟着先手拿成一样多的石子则又回到了这种情况。如果\(z<L,R\),则可以回到情况\(x<L,R\)。否则的话对应着把另外一堆变成\(z+1\)或者\(z-1\),对应着\(L<x<R\)和\(R<x<L\)两种情况。
而\(R[][]\)和\(L[][]\)是对称的,类似的求解即可。
那么最终只需要判断\(L[2][n]\)和\(a[1]\)是否相等即可判断胜负情况。
代码有点丑
#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 1010
inline int read()
{
int x=0;bool t=false;char ch=getchar();
while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
if(ch=='-')t=true,ch=getchar();
while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
return t?-x:x;
}
int n,a[MAX],L[MAX][MAX],R[MAX][MAX];
int main()
{
int T=read();
while(T--)
{
n=read();
for(int i=1;i<=n;++i)a[i]=read();
for(int i=1;i<=n;++i)L[i][i]=R[i][i]=a[i];
for(int len=2;len<=n;++len)
for(int i=1,j=i+len-1;j<=n;++i,++j)
{
int x=a[j],l=L[i][j-1],r=R[i][j-1];
if(x==r)L[i][j]=0;
else if((x>l&&x>r)||(x<l&&x<r))L[i][j]=x;
else if(r<x&&x<l)L[i][j]=x-1;
else L[i][j]=x+1;
x=a[i],l=L[i+1][j],r=R[i+1][j];
if(x==l)R[i][j]=0;
else if((x>l&&x>r)||(x<l&&x<r))R[i][j]=x;
else if(r<x&&x<l)R[i][j]=x+1;
else R[i][j]=x-1;
}
puts(a[1]==L[2][n]?"0":"1");
}
}