接触了几种基础的博弈论以后,应该多多少少都听过SG函数,SG函数能够解决大多数博弈问题,固然也能够经过SG函数找规律,而后计算结果。ios
因为本人愚昧,一直没有体会到SG的精髓,一直半懂不懂的,而后如今终于明白了,因此记录下这个神奇的SG函数。数组
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=三、mex{2,3,5}=0、mex{}=0。函数
这一步应该是很是简单的,就是定义了新的运算为mex。spa
对于任意状态 x , 定义 SG(x) = mex(F),其中F 是 x 后继状态的SG函数值的集合(就是上述mex中的数值)。最后返回值(也就是SG(X))为0为必败点,不为零必胜点。code
进一步解释一下F,就是题意中给出的能够移动的次数。举个例子来讲,一堆石子,每次只能拿1,3,5,7个,那么S数组就是1,3,5,7。blog
假如说是在一个游戏中有多个石子堆该怎么办了。咱们只须要把对每一个石子堆进行sg函数的调用,将获得的全部的值进行异或。得出来的结果为0则该情形为必败态。不然为必胜态。排序
//HDU 1847 -- Good Luck in CET-4 Everybody!
#include <iostream> #include <cstring>
#define MAXN 1010
#define MAXM 11
using namespace std; int sg[MAXN], f[MAXM]; bool Hash[MAXN]; void getSG(int m) { memset(sg, 0, sizeof(sg)); for (int i = 1; i < MAXN; i++)//枚举石子的个数
{ memset(Hash, false, sizeof(Hash)); for (int j = 0; j < m && f[j] <= i; j++) Hash[sg[i-f[j]]] = true;//枚举每次拿走的个数并标记
for (int j = 0; j < MAXN; j++) { if (!Hash[j]) { sg[i] = j; //找到这个F[](该状态能够达到的状态)中不存在的最小的数
break; } } } } int main() { int n, num = 1; for (int i = 0; i < MAXM; num <<= 1, i++) f[i] = num;//这里的F数组就是能够移动的步数,每次都是2的幂次
getSG(MAXM); while (cin >> n) { if (sg[n]) cout << "Kiki" << endl; else cout << "Cici" << endl; } return 0; }
HDU 1848 -- Fibonacci again and again (分为三个子游戏,求原游戏sg值):递归
#include <iostream> #include <cstring>
#define MAXN 1010
#define MAXM 100
using namespace std; int sg[MAXN], f[MAXM]; bool Hash[MAXN]; int getFib() { int i; f[0] = 1, f[1] = 2; for (i = 2; f[i] <= MAXN; i++) f[i] = f[i-1] + f[i-2]; return i; } void getSG(int m) { memset(sg, 0, sizeof(sg)); for (int i = 1; i < MAXN; i++) { memset(Hash, false, sizeof(Hash)); for (int j = 0; j < m && f[j] <= i; j++) Hash[sg[i-f[j]]] = true; for (int j = 0; j < MAXN; j++) { if (!Hash[j]) { sg[i] = j; break; } } } } int main() { int a, b, c; getSG(getFib()); while (cin >> a >> b >> c && (a || b || c)) { if (sg[a] ^ sg[b] ^ sg[c]) cout << "Fibo" << endl; else cout << "Nacci" << endl; } return 0; }
从以上两个题目中能够看出,f数组就是题目描述中的每次能够移动的石子数量,而getSG是相同的,具体怎么标记的能够看第一个例子中的注释。对于多堆石子,就是每一堆进行操做,而后最后将结果异或便可得出最后答案。游戏
接下来再看几个题目:ci
模板题:HDU 1536 -- S-Nim
#include <iostream> #include <algorithm> #include <cstring>
#define MAXN 10010 // 最大堆数
#define MAXM 110 // 最多有MAXM种不一样个数的取石子方法
using namespace std; int f[MAXM]; // f为可取石子数的集合
int sg[MAXN]; // sg[i]表示石子数为i时的sg函数值
bool Hash[MAXN]; // 标记一个数是否在mex{}集合中出现 // 打表预处理sg数组
void getSG(int m) { memset(sg, 0, sizeof(sg)); for (int i = 1; i < MAXN; i++) { memset(Hash, false, sizeof(Hash)); for (int j = 0; j < m && f[j] <= i; j++) Hash[sg[i-f[j]]] = true; // 当前石子数为i,i-f[i]表示由i所能达到的石子数,将其sg值标记为已出现
for (int j = 0; j < MAXN; j++) // mex(minimal excludant)运算
{ if (!Hash[j]) { sg[i] = j; break; } } } } int main() { int n, m; while (cin >> m && m) { for (int i = 0; i < m; i++) cin >> f[i]; sort(f, f + m); getSG(m); cin >> n; while (n--) { int num, sum = 0; cin >> num; for (int i = 0; i < num; i++) { int each; cin >> each; sum ^= sg[each]; } if (sum) cout << 'W'; else cout << 'L'; } cout << endl; } return 0; }
SG函数还有一个深搜版本,具体实现和循环差很少。具体以下:
//注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每一个集合只需初始化1遍 //n是集合s的大小 S[i]是定义的特殊取法规则的数组
int s[110],sg[10010],n; int SG_dfs(int x) { int i; if(sg[x]!=-1) return sg[x]; bool vis[110]; memset(vis,0,sizeof(vis)); for(i=0;i<n;i++) { if(x>=s[i]) { SG_dfs(x-s[i]); vis[sg[x-s[i]]]=1; } } int e; for(i=0;;i++) if(!vis[i]) { e=i; break; } return sg[x]=e; }
通常DFS只在打表解决不了的状况下用,首选打表预处理。具体用法以下:
仍是HDU 1536 -- S-Nim
#include <iostream> #include <algorithm> #include <cstring> #define MAXN 10010 // 最大堆数 #define MAXM 110 // 最多有MAXM种不一样个数的取石子方法 using namespace std; int m; int f[MAXM]; // f为可取石子数的集合 int sg[MAXN]; // sg[i]表示石子数为i时的sg函数值 bool Hash[MAXN]; // 标记一个数是否在mex{}集合中出现 // 加一个dfs预处理sg数组,注意sg数组须要初始化为-1,而上面打表解法须要初始化为0 // 通常首选打表预处理,难以打表才用dfs int SG_dfs(int x) { if(sg[x]!=-1) return sg[x]; bool vis[110]; memset(vis,0,sizeof(vis)); for(int i=0;i<m;i++) { if(x>=f[i]) { SG_dfs(x-f[i]); vis[sg[x-f[i]]]=1; } } int e; for(int i=0;;i++) if(!vis[i]) { e=i; break; } return sg[x]=e; } int main() { while (cin >> m && m) { for (int i = 0; i < m; i++) cin >> f[i]; sort(f, f + m); memset(sg,-1,sizeof(sg)); int n; cin >> n; while (n--) { int num, sum = 0; cin >> num; for (int i = 0; i < num; i++) { int each; cin >> each; sum ^= SG_dfs(each); } if (sum) cout << 'W'; else cout << 'L'; } cout << endl; } return 0; }
#include <iostream> #include <cstdio> #include <cstring> using namespace std; int next[6][2]={{-2,1},{1,-2},{-2,-1},{-1,-2},{-3,-1},{-1,-3}}; #define maxn 1005 int sg[maxn][maxn]; int dfs(int x,int y) { int vis[105]={0}; //注意该数组是一维的 表示该点后继的sg值的状况 if(sg[x][y]!=-1) return sg[x][y]; for(int i=0;i<6;i++) { int nx=x+next[i][0]; int ny=y+next[i][1]; if(nx>=0&&ny>=0) //注意不能不加符号就判断 等于0也是算在内的 vis[dfs(nx,ny)]=1; //由于可能走到的点的sg值尚未求过 因此要用递归深搜 //以前写的非递归是由于以前的sg值都求过了 不用搜索也能够 } for(int j=0;j<100;j++) if(!vis[j]) return sg[x][y]=j; } int main() { memset(sg,-1,sizeof(sg)); //这里定义成-1 比0 好 由于有的就是0 if的时候0还要再算一次浪费时间 int t,cas=1; cin>>t; while(t--) { int n,x,y,ans=0; cin>>n; for(int i=0;i<n;i++) { cin>>x>>y; ans^=dfs(x,y); } printf("Case %d: %s\n",cas++,ans?"Alice":"Bob"); } return 0; }
就先介绍到这,之后再慢慢补坑。