首先了解几个概念:
NIM游戏:给定N堆物品,第i堆物品有ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可以把一堆取完,但不能不取,取走最后一件物品获胜。两人都采取最优策略,问先手是否必胜。
NIM也满足下面两种游戏定义:(这些概念都不重要,理解这个想法就行)
公平组合游戏ICG:
若一个游戏满足:
1.由两名玩家交替行动;
2.在游戏的任意时刻,可以执行的合法行动与轮到那名玩家无关;(所以下棋就不按满足该条件)
3.不能行动的玩家判负;
有向图游戏:
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子,两名玩家交替地把这枚棋子延有向边进行移动,每次可以移动一步,不能移动者判负。
NIM游戏就可以把每一个状态视为一个点,从起始状态开始每个人将其移动到下一状态,不能移动的人判负
然后我们理解一件事情
先手必胜:就是在某一情况下,先手存在一种移动后可以使后手面对一种先手必败的状态;
先手必败:就是无论怎样移动,下一个状态一定是先手必胜的状态;
我们的先手必败一定有一个(0,0,0……0)
再然后我们来看一个证明:
是不是发现上面两个结论与先手必胜和先手必败的说法极其类似呢?
并且先手必败的000……0 也对应异或为0的起始条件000……0
于是我们可以认为如果a1^a2^……^an=0,则这是一种先手必败态
如果a1^a2^……^an=x(x!=0),则这是一种先手必胜态
那么我们来看一道基本例题:
代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int t;
t=1;
while (t--)
{
int ans;
int n;
cin >> n;
for (int i = 0; i < n; i++)
{
int a;
cin >> a;
if (i)
{
ans ^= a;
}
else
{
ans = a;
}
}
if (ans)
{
cout << "Yes" << endl;
}
else
{
cout << "No" << endl;
}
}
return 0;
}
下一道例题:
思路:
这既然是一个NIM问题,我们就考虑能不能用异或来做
1.对于偶数上的石子,如果对手移动偶数上的石子,无论多少个,我们只需要,他拿多少个石子放到下一奇数台阶,我们就从这个奇数台阶拿多少个到下一偶数台阶就行,这样就能保证奇数台阶上的石子数不变,直至偶数台阶上全为0;
2.由1可知,我们不需要考虑偶数台阶上的石子,只要能使奇数台阶上的石子数为0,则先手必胜;
诶,那我们就发现,这不就是异或为0吗,由我们上面的结论:如果a1^a3^……^a(2n-1)异或为0,则先手不可能使他全部为0,就是先手必败;如果a1^a3^……^a(2n-1)异或不为0,则先手可能使他全部为0,就是先手必胜。
上代码:
#include <bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
int ans = 0;
for (int i = 1; i <= n; i++)
{
int a;
cin >> a;
if (i % 2 != 0)
{
ans ^=a;
}
}
if (ans == 0)
{
cout << "No";
}
else
{
cout << "Yes";
}
return 0;
}
继续哈
我们再看一道题:
这个和上面两道题不太一样,但也是NIM游戏
要解决这个问题,我们先看几个概念:
Mex运算:设S表示一个非负数集合,定义mex(S)为求出不属于集合S的最小的一个非负整数,(比如mex{1,2,3,4}=0;mex{0,1,3,5}=2)
SG函数:(概念我不写了,举个例子吧)
这就是sg函数。
sg函数起始节点x的sg(x)=0,说明他不能够到达0这个状态;sg(x)!=0,说明他能够到达0这个状态。这不就满足任何一种非零的状态,是有可能走到0的,满足先手必胜;任何一种为0的状态,是必不可能为0的,满足先手必败
通常博弈时是不止有一个sg函数的,是有多个起始节点,因此我们就可以将所有的sg异或,如果为0,先手必败;不为0,先手必胜。(这里就是把所有的sg视为堆石子,把下一个状态视为拿石子,就跟上面一样,用异或)
对于这道题,我们就可以对每一堆石子遍历能减去的数,得到一个sg函数,求出一堆石子的sg,异或,看为不为0即可;
上代码:
#include <bits/stdc++.h>
#include <unordered_set>//哈希函数的头文件
using namespace std;
const int N = 110, M = 10010;
int f[M], a[N];
int k;
int sg(int n)
{
if (f[n] != -1)
{
return f[n];
}
unordered_set<int> S;//建立一个哈希函数用来放sg下一个状态的值,这样能快速查询到一个数有没有
for (int i = 1; i <= k; i++)
{
if (n - a[i] >= 0)
{
S.insert(sg(n - a[i]));
}
}
for (int i = 0;; i++)
{
if (!S.count(i))
{
return f[n] = i;
}
}
}
int main()
{
int n;
cin >>k;
for (int i = 1; i <= k; i++)
{
cin >> a[i];
}
cin >> n;
int ans = 0;
memset(f, -1, sizeof(f));//记忆化搜索,不然sg会算很多遍
for (int i = 1; i <= n; i++)
{
int res;
cin >> res;
ans ^= sg(res);
}
if (ans == 0)
{
cout << "No";
}
else
{
cout << "Yes";
}
return 0;
}
快结束啦,坚持一下,最后一道:
这道题就是列出每一堆的石子的下一个状态,因为每次可以递减1,所以状态很多
就是sg(100)-->sg(99,99);sg(100)-->sg(98,99)等等等等
然后就是跟上面一样求起始sg就行,只是用到了一个性质:sg(x1,x2)相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。
即sg(x1,x2)=sg(x1)^sg(x2)
代码!
#include <bits/stdc++.h>
#include <unordered_set>
using namespace std;
const int N = 110;
int f[N];
int sg(int x)
{
if (f[x] != -1)
{
return f[x];
}
unordered_set<int> S;
for (int i = 0; i < x; i++)
{
for (int j = 0; j <= i; j++)
{
S.insert(sg(i) ^ sg(j));
}
}
for (int i = 0;; i++)
{
if (!S.count(i))
{
return f[x] = i;
}
}
}
int main()
{
int n;
cin >> n;
int ans = 0;
memset(f, -1, sizeof(f));
for (int i = 0; i < n; i++)
{
int a;
cin >> a;
ans ^= sg(a);
}
if (ans)
{
cout << "Yes";
}
else
{
cout << "No";
}
return 0;
}
OK,结束!