取石子游戏
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 17630   Accepted: 5338

Description

有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。

Input

输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b,表示两堆石子的数目,a和b都不大于1,000,000,000。

Output

输出对应也有若干行,每行包含一个数字1或0,如果最后你是胜者,则为1,反之,则为0。

Sample Input

2 1
8 4
4 7

Sample Output

0
1
0

Source

NOI

这个游戏就是所谓的威佐夫博弈(Wythoff Game),很有意思。

昨天就在做这道题目,思考成果是:用一个二维数组来表示玩家将面临的局面,即a[i][j]表示两堆石头分别有i个和j个,如果a[i][j]是必败局面的话,那么——这个点的右侧所有点a[i][k](k>j)都是必胜点,因为可以通过拿走第二堆的k-j个石子令对方面临必败局面a[i][j];这个点的下侧所有点a[k][j](k>i)都是必胜点,因为可以通过拿走第二堆的k-i个石子令对方面临必败局面a[i][j];这个点的右下方45度所有点a[i+k][j+k](k>0)都是必胜点,因为可以通过同时拿走两堆的k个石子令对方面临必败局面a[i][j]。这样如果在二维数组a[i][j]处标记”X”表示必败的话,在上面提到的三类点的位置都可以标记”O”表示必胜了,做完这项工作后,再挑选如今距离原点最近的未被标记的点,它一定是下一个必败点——因为它无法通过游戏规则移动到一个必败点,并且规则规定的动作都是朝向原点移动的,而它是距离原点最近的未被标记的点,因此它只能移动到一个必胜点从而让对方获胜,所以该点一定是下一个必败点。有了新必败点后就可以重复上述工作,直到找出问题范围内的所有必败点。

然而这样的模拟工作是不被允许的,因为数据量太大了,因此这道题要求的是给定两个数i,j,有办法立刻判断a[i][j]是不是必败点,在O(1)时间给出解答。

我试图找到必败点的规律,但是没成功,以上就是昨天的思考结果。因为这个问题够简洁,所以我坚信一定有简单规律的,因此在考察了很大数据量仍然没有发现规律后,我无比好奇这道题的解法。今天上网一搜才知道规律果然够简单够经典,竟然是“黄金分割”,以下内容来自对网上内容的总结:

前几个必败点如下:(0,0),(1,2),(3,5),(4,7),(6,10),(8,13)……可以发现,对于第k个必败点(m(k),n(k))来说,m(k)是前面没有出现过的最小自然数,n(k)=m(k)+k。一个必败点有如下性质:

1.所有自然数都会且仅会出现在一个必败点中;

证明:m(k)是前面没有出现过的最小自然数,自然与前k-1个必败点中的数字都不同;m(k)>m(k-1),否则违背m(k-1)的选择原则;n(k)=m(k)+k>m(k-1)+(k-1)=n(k-1)>m(k-1),因此n(k)比以往出现的任何数都大,即也没有出现过。又由于m(k)的选择原则,所有自然数都会出现在某个必败点中。性质1证毕。

2.规则允许的任意操作可将必败点移动到必胜点;

证明:以必败点(m(k),n(k))为例。若只改变两个数中的一个,由于性质1,则得到的点一定是必胜点;若同时增加两个数,由于不能改变两数之差,又有n(k)-m(k)=k,故得到的点也一定是必胜点。性质2证毕。

3.一定存在规则允许的某种操作可将必胜点移动到必败点;

证明:以某个必胜点(i,j)为例。因为所有自然数都会出现在某个必败点中,故要么i等于m(k),要么j等于n(k)。若i=m(k),j>n(k),可从j中取走j-n(k)个石子到达必败点;若i=m(k),jm(k),j=n(k),可从i中取走i-m(k)个石子到达必败点;若i

判断一个点是不是必败点的公式与黄金分割有关,为:

m(k) = k * (1 + sqrt(5))/2

n(k) = m(k) + k

至于为什么如此,我就不知道了,也没有查到,很好奇。

POJ1067就是这道题,代码如下:

#include 
#include 
 
using namespace std;
 
int main()
{
	int m,n;
	while(cin>>m>>n)
	{
		if (m > n)
		{
			int temp;
			temp = m;
			m = n;
			n =temp;
		}
		int k = n - m;
		int data = floor(k*(1.0+sqrt(5.0))/2.0);
		if (data == m)
			cout<<0<<endl;
		else
			cout<<1<<endl;
	}
}

需要注意的是,由于数据量过大,float的精度不够,而只能用double,因此代码中的小数没有加“f”。

转自http://www.sunhongfeng.com/2010/04/poj1067/,感谢作者chris
我自己想到了一种更简便的递推方式,用一维数组a[]和计数t,首先i=t=1,a[i]和a[i+t]构成一个必败组,执行a[i]=i+t,a[i+t]=i,然后i自加,找到之前从未被赋值过的数,如:当i=1时,1与2被赋值,这时i要自加两次,i=3再把3和5赋值。如此即可找到所有必败组。但因时间耗费太大也不允许。不过收获总算颇丰。