P2599 [ZJOI2009]取石子游戏

斯德哥尔摩

2018-07-28 22:52:35

Personal

[P2599 [ZJOI2009]取石子游戏](https://www.luogu.org/problemnew/show/P2599) 博弈论的$Nim$游戏中比较难的题。 首先,讲个性质: 取石子时,当先手使得左右两边的石子相等时,可以保证先手必胜。 因为当先手取到左右石子相等的局面时,则后手无论在哪一堆取石子,接下来先手都可以有相同的方法取另一堆石子。 这样又回到了原来的状态(左右石子相等),则无论如何先手都有办法应对后手的取法。 设在区间$[i,j]$左边放$Left[i][j]$(可以为0)个石子后(区间内石子保持原状),产生的局面为**先手必败局面**,$Right[i][j]$就是在右边放,定义类似。 设$val[i]$为第i堆石子个数。 最后我们只用看$Left[2][n]$是否等于$val[1]$就可以了。(如果等于就说明这个情况先手必败,否则先手必胜) 这一题可以注意到对于一段区间$[L,R]$, 若$L+1$到$R$的石子数固定, 那么使得在这段区间上先手必败的$val[L]$有且仅有一个。 接下来证明$Left[i][j]$是唯一的: 1. 若存在两或以上个$Left[i][j]$,则从某一个必定可一步转移到另一个,矛盾,故最多只有一个。 2. 若没有$Left[i][j]$,那么必胜态只可能通过拿右边的石子转移到某个必败态。 由于此时左边石子可以是任意个,右边石子数固定,于是必有右边的某一个石子数对应了多种必败态。 这些必败态只有左边的石子数不同,与1矛盾,舍去。 最后我们考虑$Left[i][j]$的求法,分类讨论: 设$l=Left[i][j-1],r=Right[i][j-1],w=val[j]$。通过下面的分析我们可以发现#Left[i][j]#只和$l,r,w$三个数有关. 1. 边界条件:$Left[i][i]=Right[i][i]=val[i]$. 两堆一样的,后手照着先手拿的数量在另一堆拿即可。 2. 首先,最容易想到的是$r==w$的情况,这时$[i,j]$这段区间已经先手必败,那么$Left[i][j]==0$。 3. 若$w<l,w<r$,则$Left[i][j]=w$。 因为此时当两堆石子相同时,后手照着先手取,这样先手一定会先拿完某一堆,后手占据主动。 这之后又会出现某一种我们分类讨论中的情况,相当于一种递归,直到后手获胜。 4. 若$w<l,w>r$,则$Left[i][j]=w-1$。 此时如果先手在左边拿使其个数变为y,若$y<r$,则后手在右边也拿到y即可变为情况3; 若$y>=r$,则后手在右边拿到$y + 1$,于是回到了相同的情况。 若先手在右边拿到$y$,若$y<r$,同样可到情况3; 若$y==r$, 直接把左边的堆拿完,先手便面临败局; 若$y>r$,则在左边拿到$y-1$,回到相同情况。 5. 若$w<r,w>=l$,则$Left[i][j]=w+1$,与情况4恰好相反。 6. 若$w>l,w>r$,则$left[i][j]=w$。 若先手将某一堆拿到了$y$,且$y>r$, 跟着先手拿即可回到相同情况; 若$y<l$,回到情况3; 若在在左边拿到$l$或在右边拿到$r$,后手拿掉另一堆即可; 否则根据情况在另一堆拿到$y-1$或是$y+1$,分别对应情况4和5。 $Right[i][j]$的求法与$Left[i][j]$完全对称,照搬即可。 最后只需判断$val[1]==Left[2][n]$。 附代码: ```cpp #include<iostream> #include<algorithm> #include<cstdio> #include<cmath> #define MAXN 1010 using namespace std; int n; int val[MAXN],Left[MAXN][MAXN],Right[MAXN][MAXN]; inline int read(){ int date=0,w=1;char c=0; while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9'){date=date*10+c-'0';c=getchar();} return date*w; } void work(){ if(val[1]==Left[2][n])printf("0\n"); else printf("1\n"); } void init(){ n=read(); for(int i=1;i<=n;i++)Left[i][i]=Right[i][i]=val[i]=read(); for(int k=1;k<=n;k++) for(int i=1;i+k<=n;i++){ int j=i+k,l=Left[i][j-1],r=Right[i][j-1],w=val[j]; if(r==w)Left[i][j]=0; else if(l>w&&r>w)Left[i][j]=w; else if(l<=w&&r>w)Left[i][j]=w+1; else if(l>w&&r<w)Left[i][j]=w-1; else Left[i][j]=w; l=Left[i+1][j];r=Right[i+1][j],w=val[i]; if(r==w)Right[i][j]=0; else if(l>w&&r>w)Right[i][j]=w; else if(l<=w&&r>w)Right[i][j]=w+1; else if(l>w&&r<w)Right[i][j]=w-1; else Right[i][j]=w; } } int main(){ int t=read(); while(t--){ init(); work(); } return 0; } ```