P1247 取火柴游戏
原题链接 https://www.luogu.com.cn/problem/P1247
题解
这是一道经典的 NIM游戏 的博弈论 。
NIM游戏
通常的 Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是 “ 选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
设有 n 堆石子,每堆石子的数量为 a1 , a2 , …… , an ;
当某个人面临 a1 = a2 = …… = an = 0 的状态时,这个人就输了,所以我们将此状态称为 “ 必败状态 ”;
如果一个状态能通向必败状态,那么就称当前状态为 “ 必胜状态 ”,因为两个人都是绝顶聪明的,能赢绝不输;
我们先从最简单的必败状态开始分析:
若有 a1 = a2 = …… = an = 0,那么可以得到 a1 ^ a2 ^…… ^ an = 0;
话说虽然 a1 ^ a2 ^…… ^ an = 0 不能推得 a1 = a2 = …… = an = 0,但是你若想让对手面临 a1 = a2 = …… = an = 0 的状态,你起码要保证把 a1 ^ a2 ^…… ^ an = 0 的状态留给对手好吧;
分析一下将 a1 ^ a2 ^…… ^ an = 0 的状态留给对手的结果:
①. a1 = a2 = …… = an = 0
皆大欢喜,你赢了!
②. 有 i 对相等的数(1 <= i <= n/2)
虽然你现在没有赢,但是对手肯定赢不了,因为此时至少有两堆石子没有被取空,你的对手不可能同时将两堆石子取空;
这么一看,你若把 a1 ^ a2 ^…… ^ an = 0 的状态留给对面,除了能把对手恶心死好像就没有什么坏处了,何乐而不为呢?
但是你现在的状态是 a1 ^ a2 ^…… ^ an = x 啊,那要怎么办呢?
根据异或的性质可以得到:a1 ^ a2 ^…… ^ an ^ x = 0,如果我们对其中一堆石子 ai 取得只剩下 ai' = ai ^ x 个,那么此时留给对手的状态就是 a1 ^ a2 ^… ^ ai' ^ … ^ an = 0 了 。
通过上述操作可知,若对手面临 a1 ^ a2 ^…… ^ an = 0 的状态,无论ta接下来怎么取,一定会将状态转化为 a1 ^ a2 ^…^ ai' ^… ^ an = y ( y≠0 ) ,然后我们可以反手再将状态变为 a1 ^ a2 ^…^ ai' ^…^ aj' ^… ^ an = 0 ( aj' = aj ^ y ) ,这样对手就一直面临着 a1 ^ a2 ^…… ^ an = 0 的状态,也就自然不会有机会赢了 。
总结:a1 ^ a2 ^…… ^ an = x 是一个必胜状态;反之,a1 ^ a2 ^…… ^ an = 0 是一个必败状态(两种状态的胜负情况都是对于当前操作者来说的)。
还有最后一个问题:当面临 a1 ^ a2 ^…… ^ an = x 的状态时,我是将哪一堆的 ai 取成 ai' = ai ^ x 呢?
看到题目中给出的游戏规则:每次可以从一堆中取走若干根火柴,也可以一堆全部取走,但不允许跨堆取,也不允许不取。
所以我们要保证 ai' < ai ,即 ai ^ x < ai 。
设 x 在二进制表示下最高位的 1 是在第 k 位,由异或的性质可知:a1 ~ an 的若干数中(至少一个)的第 k 位为 1,然后我们随便从中找出一个数,设为 ai;
思考一下 ai ^ x < ai ?我们对 ai 和 x 的二进制位进行分析:
由于 x 的最高位在第 k 位,所以第 k 位往左的部分异或后不变; 第 k 位异或后为 0;
第 k 位由 1 变为了 0,ai 的大小减去 2k-1 ,就算后面的 1~k-1 位由 0000……000 变成了 1111……111,ai 的大小最多增加 20 + 21 + …… + 2k-2 = 2k-1 - 1 。
这么看来,ai 的值注定是要减小的了,那么 ai' < ai ,则 ai 就是我们要找的天选之子。
Code:
#include<iostream> #include<cstdio> using namespace std; const int N=500005; int n; int a[N]; long long ans; int main() { scanf("%d",&n); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); ans^=a[i]; //算出每堆石子数的异或和 } if(ans==0) printf("lose\n"); //ans==0是必败状态 else //否则为必胜状态 { //我们接下来的任务就是要找一个ai把它取成ai^ans int x=0; for(int i=45;i;i--) //寻找ans的最高位的1是第几位 { if(ans&(1ll<<i)) { x=i; break; //由于我们是从高位向低位枚举,所以一旦找到,那么肯定是最高位,直接跳出 } } for(int i=1;i<=n;i++) //我们再从所有的石子堆里找出一个ai,使得ai的第x位也是1 { if(a[i]&(1ll<<x)) //找到符合条件的ai { printf("%d %d\n",a[i]-(a[i]^ans),i); //将第i堆石子取得只剩下ai^ans,那就要取走ai-(ai^ans) a[i]=a[i]^ans; //剩下ai^ans for(int j=1;j<=n;j++) printf("%d ",a[j]); //将剩下每堆得石子数输出即可 return 0; } } } return 0; }