接觸了幾種基礎的博弈論以後,應該多多少少都聽過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; }
就先介紹到這,之後再慢慢補坑。