USTCOJ 1213 取石子游戏 (经典NIM问题)及一些扩展 与(&) 或 (|)异或 (^)运算性质

本文介绍了USTCOJ 1213题目的解题思路,该题目涉及经典的NIM游戏。作者探讨了NIM游戏的必胜策略,并通过异或运算的性质寻找第一步的最优解。文章阐述了异或、与、或运算的交换律、结合律等性质,并证明了在NIM游戏中,非0的NIM数意味着存在必胜策略。最后,作者证明了在给定条件下总能找到一个a[k]使得b[k] < a[k],从而完成游戏的转变。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:http://acm.ustc.edu.cn/ustcoj/problem.php?id=1213

在做本题之前,虽然听说过NIM游戏,但并不知道其必胜策略,之前遇到的另一个NIM问题是两堆石子的版本,取石子规则又略有不同,在思考两天之后终于解决了,结论非常优美,用到了数学竞赛知识里的Betty定理,结论和黄金分割有关,这里给个POJ百练上的题目链接:http://poj.grids.cn/practice/1067/。在自己解决这两道题之后,决定深入理解一下NIM问题,于是查阅了一些资料,发现已经有很多人探讨过这个问题,看来能想出这个问题的人很多啊,但是还是挺有成就感的,而且发现自己采用的方法与文献上的类似,(对于后一个问题也用了Betty定理证明),因此还是很乐意写篇报告来总结一下自己的心得。(Ps个人认为这种经典的问题一定要自己想出,而且要严格证明出来,不然再遇到也是不一定能很快解决的)。

首先,正如题目中已经给出的结论:我们定义一个NIM数,然后可以通过异或运算直接算出这个数字,非0则存在必胜策略(这里先不给出这个结论的证明,我决定在下一篇文章中给出,因为正是解决那道题目时给我的启发并一并联想到NIM问题,才完成了证明)。我们现在先默认结论正确,我们先专注于这道题目的要求:找出第一步最少需要取走的石子数,使NIM数变为0。

我在遇到这个问题时,最先的反应的是异或运算有什么性质啊?是不是要用到啊?可是在此之前并没有研究过这些运算:与,或, 异或的性质,所以我先从这些运算下手(我感觉这些运算有共同性,应该一起研究,又发现这三个运算只针对二进制位的某一位进行操作,不同位不会影响,所以只需要研究一个二进制位上的四种情况即可):

得到的结论是:

三者共性是:

1:满足交换律 a*b = b*a  其中*表示三个运算中的任意一个。

2:满足结合律 a*b*c = (a * b) * c = a * (b * c)  其中*表示三个运算中的任意一个。

注意不一定满足分配律:如 0 ^ (1 & 0)!= (0 ^1)& (0 ^ 0)。

不同有挺多:

列举一些:

1:异或  a ^ a = 0    a ^ 0 = a  假设 a ^ b  = c 那么 a ^ c = b  b ^ c = a (我发现这个结论很有用, 这题也需要), 这里c 与 a, b 大小关系不确定。

2 :与 a & a = a   a & 0 = 0   假设 a & b  = c 那么 a & c = c  b & c = c  且 c 是a, b,c中较小的那一个(可以相等)

3 : 或 a | a = a    a | 0 = a   假设 a | b = c 那么 a | c = c  b | c = c  且 c 是 a,b, c中较大的那一个(可以相等)

4 : 一个小技巧 :a =  a & (a + 1)把a的二进制位最右边的1改写为0,类似可以研究一下a & (a - 1)之类,不再赘述

有了以上性质,我们着手本题,并非全部用到,只用到一两条吧:

首先肯定得算出NIM数没什么快的方法了,直接O(n)异或运算一遍,

如果不等于0, 需要考虑进行改动了: 假设我们最终决定改动第k个数a[k] --> b[k],那么应该有 b[k] ^ ( a[0] ^ a[1] ^ a[2] ......^ a[k - 1] ^ a[k + 1] ......^ a[n - 1] ) = 0

所以 b[k] = ( a[0] ^ a[1] ^ a[2] ......^ a[k - 1] ^ a[k + 1] ......^ a[n - 1] )  (与本身异或才为0)

那么怎么算 ( a[0] ^ a[1] ^ a[2] ......^ a[k - 1] ^ a[k + 1] ......^ a[n - 1] ) 呢?利用异或性质 a[k] ^ ( 一坨 异或a) = NIM, 所以 a [k] ^ NIM = (一坨异或 a) 

所以 再做一遍异或就可以求出b[k], 考虑到异或结果不一定比a[k] 小,所以 如果b[k] > a[k] 则 a[k] 不能通过取走石子(减少)而变成b[k] , 而对于b[k] <= a[k], 则可行,取其中

a[k] - b[k] 最小即可。

代码如下:

#include<iostream>
#define N 1000010
using namespace std;
int a[N] = {0};

int main()
{
		int re, i, n, nim, s;

		while (cin >> n, n)
		{
				nim = 0;
				s = 0;
				for (i = 0; i < n; ++i)
				{
						cin >> a[i];
						nim ^= a[i];
				}
				if (nim == 0)
						cout << -1 << endl;
				else
				{
						for (i = 0; i < n; ++i)
						{
								if (s == 0)
								{
										if (a[i] >= (nim ^ a[i]))
										{
												re = a[i] - (nim ^ a[i]);
												s = 1;
										}
								}
								else										
								{
										if ((nim ^ a[i]) <= a[i])
												re = a[i] - (nim ^ a[i]) < re ? a[i] - (nim ^ a[i]) : re;
								}
						}
						cout << re << endl;
				}
		}	
		return 0;
}
这个算法时间复杂度应该算O(nlog(n))吧,broad上有更快的代码,不知道如何实现的还是只是进行了常数级优化,如有更好的办法,希望评论让我知道,有错误也请指出

以上均是个人的思路,解题过程,希望要不被鄙视。。。。。。

一点补充:由以上代码的正确性,引发了我另一个思考:一定能找到一个a[k]使b[k] < a[k]么?因为代码正确,所以结论是对的咯,怎么证明呢?因为NIM数不等于0,所以二进制最高位为1,这个1怎么异或得到呢?必然有奇数个a[i]的这一位上有1,取其中任意一个,与NIM异或将把这个1变为0,得到的数就会比 a[i] 小(a[i]原来比这个1所在位高的位上数字全不变,而这一位变小,整个数字变小)因此结论成立。





评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值