SG函數

接觸了幾種基礎的博弈論以後,應該多多少少都聽過SG函數,SG函數能夠解決大多數博弈問題,固然也能夠經過SG函數找規律,而後計算結果。ios

因爲本人愚昧,一直沒有體會到SG的精髓,一直半懂不懂的,而後如今終於明白了,因此記錄下這個神奇的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;
}

 

LightOJ 1315Game of Hyper Knights,此題打表很差處理,只好DFS。

#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;
}

 

就先介紹到這,之後再慢慢補坑。

相關文章
相關標籤/搜索