P1247 取火柴游戏

原题链接  https://www.luogu.com.cn/problem/P1247

 

 

题解 

这是一道经典的 NIM游戏 的博弈论 。

NIM游戏

通常的 Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是 “ 选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

设有 n 堆石子,每堆石子的数量为 a1 , a2 , …… , an

当某个人面临 a1 = a= …… = an = 0 的状态时,这个人就输了,所以我们将此状态称为 “ 必败状态 ”

如果一个状态能通向必败状态,那么就称当前状态为 “ 必胜状态 ”,因为两个人都是绝顶聪明的,能赢绝不输;

我们先从最简单的必败状态开始分析:

若有 a1 = a= …… = an = 0,那么可以得到 a1 ^ a2 ^…… ^ an = 0;

话说虽然 a1 ^ a^…… ^ an = 0 不能推得 a1 = a= …… = an = 0,但是你若想让对手面临 a1 = a= …… = an = 0 的状态,你起码要保证把 a1 ^ a^…… ^ an = 0 的状态留给对手好吧;

分析一下将 a1 ^ a^…… ^ an = 0 的状态留给对手的结果:

①. a1 = a= …… = an = 0

皆大欢喜,你赢了!

②. 有 i 对相等的数(1 <= i <= n/2)

虽然你现在没有赢,但是对手肯定赢不了,因为此时至少有两堆石子没有被取空,你的对手不可能同时将两堆石子取空;

这么一看,你若把 a1 ^ a^…… ^ an = 0 的状态留给对面,除了能把对手恶心死好像就没有什么坏处了,何乐而不为呢?

但是你现在的状态是 a1 ^ a^…… ^ an = x 啊,那要怎么办呢?

根据异或的性质可以得到:a1 ^ a^…… ^ an ^ x = 0,如果我们对其中一堆石子 ai 取得只剩下 ai' = ai ^ x 个,那么此时留给对手的状态就是 a1 ^ a^… ^ ai' ^ … ^ an = 0 了 。

通过上述操作可知,若对手面临 a1 ^ a^…… ^ an = 0 的状态,无论ta接下来怎么取,一定会将状态转化为 a1 ^ a^…^ ai' ^… ^ an = y ( y≠0 ) ,然后我们可以反手再将状态变为 a1 ^ a^…^ ai' ^…^ aj' ^… ^ an = 0 ( aj' = aj ^ y ) ,这样对手就一直面临着 a1 ^ a^…… ^ an = 0 的状态,也就自然不会有机会赢了 。

总结:a1 ^ a^…… ^ an = x 是一个必胜状态;反之,a1 ^ a^…… ^ an = 0 是一个必败状态(两种状态的胜负情况都是对于当前操作者来说的)。

还有最后一个问题:当面临 a1 ^ a^…… ^ 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;
}

 

posted @ 2020-06-21 08:50  暗い之殇  阅读(167)  评论(0编辑  收藏  举报