题目链接: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
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
#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;
}