SG函數


接觸了幾種基礎的博弈論之后,應該多多少少都聽過SG函數,SG函數可以解決大多數博弈問題,當然也可以通過SG函數找規律,然后計算結果。

由於本人愚昧,一直沒有體會到SG的精髓,一直半懂不懂的,然后現在終於明白了,所以記錄下這個神奇的SG函數。

SG函數:

首先定義mex(minimal excludant)運算,這是施加於一個集合的運算,表示最小的不屬於這個集合的非負整數。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

這一步應該是非常簡單的,就是定義了新的運算為mex。

對於任意狀態 x , 定義 SG(x) = mex(F),其中F 是 x 后繼狀態的SG函數值的集合(就是上述mex中的數值)。最后返回值(也就是SG(X))為0為必敗點,不為零必勝點。

進一步解釋一下F,就是題意中給出的可以移動的次數。舉個例子來說,一堆石子,每次只能拿1,3,5,7個,那么S數組就是1,3,5,7。

假如說是在一個游戲中有多個石子堆該怎么辦了。我們只需要把對每個石子堆進行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是相同的,具體怎么標記的可以看第一個例子中的注釋。對於多堆石子,就是每一堆進行操作,然后最后將結果異或即可得出最后答案。

 

接下來再看幾個題目:

模板題: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;
}

 

就先介紹到這,以后再慢慢補坑。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM