牛客小白赛13,I题NIM游戏

题目链接:I 小A取石子

很久没做nim游戏了,与之相关的博弈还有SG函数,还没看。

先浅谈一下我对nim游戏的理解:

拿取石子来说,任意一方取的时候都是从某堆中任取。

两个人玩游戏,若当前局面是平衡的(即nim和为0)则先手必输,否则先手必胜。

简单例子,两堆石子个数分别为:1,1。显然先手必败,这个时候的nim和为0。

       如果两堆石子个数分别为:1,2,这时候有了前者经验,把取完石子后 1,1的局面留给对手就能赢,所以先手必胜,。此时nim和为3。

       用二进制分析一下,1D=01B,2D=10B,NIM_SUM=1D^2D=01B^10B=11B=3。前面说到,当前nim和为0时必败,那要让对手处于必败局面,就需要nim和在自己取走石子后变为0。how to do this?取走1显然不行,将2全部取走也显然不行。考虑到二进制异或操作时,奇数个1异或结果为1,偶数个1异或结果为0,则用当前nim和异或某堆石子得到的结果应当就是多出来的、应当被取走的石子个数(设为x),这个个数显然应该要小于当前堆中石子数,否则不能取,当前堆取走x个石子不是可选择的方案。

 一般情况:

 

23=8

22=4

21=2

20=1

大小为7的堆

0

1

1

1

大小为9的堆

1

0

0

1

大小为12的堆

1

1

0

0

大小为15的堆

1

1

1

1

 

23=8

22=4

21=2

20=1

大小为7的堆

0

1

1

1

大小为9的堆

1

0

0

1

大小为12的堆

0

0

0

1

大小为15的堆

1

1

1

1

 

23=8

22=4

21=2

20=1

大小为7的堆

0

1

1

1

大小为9的堆

0

1

0

0

大小为12的堆

1

1

0

0

大小为15的堆

1

1

1

1

考虑各堆大小分别为N1,N2,……Nk的一般的Nim取子游戏。

将每一个数Ni表示为其二进制数(数的位数相等,不等时在前面补0):

N1 = as…a1a0

N2 = bs…b1b0

……

Nk = ms…m1m0

如果每一种大小的子堆的个数都是偶数,我们就称Nim取子游戏是平衡的,而对应位相加是偶数的称为平衡位,否则称为非平衡位。因此,Nim取子游戏是平衡的,当且仅当:

as + bs + … + ms 是偶数

……

a1 + b1 + … + m1 是偶数

a0 + b0 + … + m0是偶数

于是,我们就能得出获胜策略:

游戏人I(先手)能够在非平衡取子游戏中取胜,而游戏人II(后手)能够在平衡的取子游戏中取胜

引入概念Nim-Sum

n定义: 假设 (xm · · · x0)2 (ym · · · y0)2 nim-sum(zm · · · z0)2,则我们表示成 (xm · · · x0)2 ⊕ (ym · · · y0)2 = (zm · · · z0)2, 这里zk = xk + yk (mod 2)k=0…m.

对于nim游戏的某个位置(x1,x2,x3),当且仅当它各部分的nim-sum等于0时(即x1⊕x2⊕x3=0),则当前位于必败点。

可行的操作方案的选取规律:

n使用这个计算出来的nim-sum再次分别与三个堆中元素个数进行异或操作,如果得到异或的结果小于堆数则为可选的必胜的操作。

好了,到此就足以解决题目了。要注意的时A在游戏前取走石子时要么取K个要么不取,只要能让游戏在开始前的nim_sum不为0,则小A必胜,否则必败。

AC代码:

#include<iostream>
using namespace std;
const int maxn=1e5+10;
int a[maxn];
int main(){
	int n,k;
	int nim_sum=0;
	cin>>n>>k;
	bool flag=true;
	for(int i = 0 ; i < n ; i ++){
		cin>>a[i];
		nim_sum^=a[i];
	}
	if(nim_sum==0){//当前小A必败
		for(int i = 0 ; i < n ; i ++){
			int temp=nim_sum^a[i];
			if(k==temp && k<=a[i]){
				cout<<"YES";
				return 0;
			}
		}
		cout<<"NO";
	}
	else cout<<"YES";
	return 0;
} 

上面是自己写的代码,发现还是有些冗余。首先,0^a[i]等于a[i],其次,nim_sum==0是只要能取走k就一定使得nim_sum!=0

所以还是标程要简洁得多:https://ac.nowcoder.com/acm/contest/view-submission?submissionId=40519576

#pragma GCC optimize(3,"Ofast","inline")
#include<bits/stdc++.h>
#define pb push_back
#define Rep(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
typedef long long ll;
typedef long double ld;
const int inf=0x3f3f3f3f;
const ll INF=9e18;
const int N=1e5+50;
int a[N];
int main() {
    int n,k,ans=0;
    bool flag=false;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        ans^=a[i];  
        if(a[i]>=k) flag=true;
    }
    //ans==0表示当前小A必败
    //&&后面判断nim_sum在当前局面下会否发生变化
    if((!ans)&&((!flag)||k==0)) printf("NO");
    else printf("YES");
    return 0;
}

 

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值