题目链接: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所在位高的位上数字全不变,而这一位变小,整个数字变小)因此结论成立。