概述
这篇博客针对满足如下情形的博弈展开
- A、B交替进行某种游戏规定的操作,每操作一次,选手可以在有限的操作(操作必须合法)集合中任选一种
- 对于游戏的任何一种可能的局面,合法的操作集合只取决于这个局面本身,不取决于其它因素(跟选手,以前的所有操作无关)
- 如果当前选手无法进行合法的操作,则为负
SG函数
一个经典的拿石子的情景
- 有一堆大小为 n n n的石子,现在双方开始轮流拿石子,每一次可以拿走位于集合 { 1 , 2 , 3 } \{1,2,3\} {
1,2,3}中的那么多个石子,轮到谁不能拿了谁就输。
这种情景比较直观的一个想法是,可以通过动态规划求解。设 d p [ i ] dp[i] dp[i]表示当前石子的数量为 i i i时是否能赢,则状态转移方程为 d p [ i ] = ! ( d p [ i − 1 ] & & d p [ i − 2 ] & & d p [ i − 3 ] ) dp[i]=!(dp[i-1] \&\& dp[i-2] \&\& dp[i-3]) dp[i]=!(dp[i−1]&&dp[i−2]&&dp[i−3]),这种想法很直观,如果当前状态是必胜态,那么必定在拿走某数量的石子后可以将状态转移为必败态。
SG函数
- 先定义 m e x ( m i n i m a l e x c l u d a n t ) mex(minimal excludant) mex(minimalexcludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如 m e x { 0 , 1 , 2 , 4 } = 3 、 m e x { 2 , 3 , 5 } = 0 、 m e x { } = 0 mex\{0,1,2,4\}=3、mex\{2,3,5\}=0、mex\{\}=0 mex{ 0,1,2,4}=3、mex{ 2,3,5}=0、mex{ }=0。
- 对于任意状态 x x x , 定义 S G ( x ) = m e x ( S ) SG(x) = mex(S) SG(x)=mex(S),其中 S S S是 x x x后继状态的 S G SG SG函数值的集合。如 x x x有三个后继状态分别为 a , b , c a,b,c a,b,c,那么 S G ( x ) = m e x { S G ( a ) , S G ( b ) , S G ( c ) } SG(x)=mex\{SG(a),SG(b),SG(c)\} SG(x)=mex{ SG(a),SG(b),SG(c)}。 这样集合 S S S的终态必然是空集,所以 S G SG SG函数的终态为 S G ( x ) = 0 SG(x)=0 SG(x)=0。当且仅当 x x x为必败态 P P P时, S G ( x ) = 0 SG(x)=0 SG(x)=0。
SG函数的意义
- 对于任何一个状态 x x x来说,如果 S G ( x ) ! = 0 SG(x)!=0 SG(x)!=0,说明这个状态可以通过某种操作转移到必败态,这时对手将必败;否则对手将必胜。这是从布尔意义来对 S G SG SG函数进行的解释。
- 但是 S G SG SG函数并没有像前面用动态规划思想分析的那样,只定义两种取值。实际上, S G SG SG函数还有更巧妙的性质,如果 S G ( x ) = k SG(x)=k SG(x)=k,那么意味着 x x x后面的状态的 S G SG SG函数值是 [ 0 , k − 1 ] [0,k-1] [0,k−1]中的全部整数。这个性质的巧妙会在NIM博弈中体现出来。
NIM博弈
上面的取石子的游戏情景如果变成了给出 k k k堆石子,每次可以选任意一堆取一定数量个的话,这个问题的解就是所有石子堆中的石子数量的 S G SG SG函数的异或值。如果异或值非0则先手必胜;反之,先手必败。
我们可以这样想这个问题,如果异或值为0,那么无论先手取多少数量都一定会导致异或值不为0,这时候,后手只需要取一定数量的石子使得异或值保持为0。随着石子数量的减少,异或值始终会在后手变为0,因此先手必胜。
如果异或值非0,设这个非零值为 x x x。那么对于非0值的最高位的1来说(不妨设为 k k k,这个数字为 n n n)。我们可以拿掉一定的数量,使得剩余值等于 x x x,这样剩下的异或值就是0。
HDU1524 AC代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1010;
int sg[maxn], vis[maxn], n, m;
vector<int> b[maxn];
int mex(set<int> &s){
for(int i=0;i<=s.size();++i)
if(s.find(i) == s.end()) return i;
}
int getSG(int cur){
int i;
if(b[cur].empty()){
vis[cur] = 1;
return 0;
}
if(vis[cur]) return sg[cur];
int hash[maxn] = {
0};
for(i=0;i<b[cur].size();++i){
hash[getSG(b[cur][i])] = 1;
}
vis[cur] = 1;
for(i=0;i<=n;++i){
if(!hash[i]){
sg[cur] = i;
break;
}
}
return sg[cur];
}
int main(){
int t, i, j, x;
ios::sync_with_stdio(false);
while(cin>>n){
memset(sg, 0, sizeof(sg));
memset(vis, 0, sizeof(vis));
for(i=0;i<n;++i) b[i].clear();
for(i=0;i<n;++i){
cin>>x;
for(j=0;j<x;++j){
cin>>t;
b[i].push_back(t);
}
}
for(i=0;i<n;++i) getSG(i);
while(cin>>m){
if(!m) break;
int ans = 0;
for(i=0;i<m;++i){
cin>>j;
ans ^= sg[j];
}
if(ans){
cout<<"WIN\n";
}else{
cout<<"LOSE\n";
}
}
}
return 0;
}
HDU1848 AC代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1010;
int sg[maxn], f[maxn];
void getSG(){
int i, j;
f[1] = 1;
f[2] = 2;
for(i=3;i<=1000;++i){
f[i] = f[i-1] + f[i-2];
if(f[i] > 1000) break;
}
int sz = i;
for(i=1;i<=1000;++i){
int vis[maxn] = {
0};
for(j=1;j<sz;++j){
if(i >= f[j]){
vis[sg[i - f[j]]] = 1;
}
}
for(j=0;j<=1000;++j){
if(!vis[j]){
sg[i] = j;
break;
}
}
}
}
int main(){
int n, m, p;
ios::sync_with_stdio(false);
getSG();
while(cin>>m>>n>>p){
if(!m && !n && !p) break;
int ans = sg[m] ^ sg[n] ^ sg[p];
if(ans){
cout<<"Fibo\n";
}else{
cout<<"Nacci\n";
}
}
return 0;
}