1. 程式人生 > >博弈のSG函式

博弈のSG函式

遇到博弈的一些題目不能直接找規律,但是一些問題是有類似於分治的思想,就是大問題能轉為更小的子問題,如:100個石頭拿5個走,那問題就簡化為95個石頭。

SG即用一個有向圖表示輸贏狀態,圖中的結點值為0表示先手必敗,非0為先手勝點,即:

對於一個局面x,它的sg值:

(1)當前局面x為終結局面,則sg[x] = 0;

(2)當前局面x不是終結局面,則sg[x] = mex{ sg[y] | y為x的後繼局面};

mex{ a[i] } 表示a中未出現的最小非負整數。舉個栗子:mex{0, 1, 2} = 3, mex{1, 2}=0, mex{0,1,3}=2;

程式怎麼求mex{}呢?標記那些sg[y]的vis值為1,最後從0開始遍歷vis陣列,第一個為0的就是mex{}。

可以發現

(1)若一個局面x為終結局面,則有sg(x)=0;否則sg(x)>0。
(2)若一個局面x,其sg(x)>0,則一定存在一個後續局面y,sg(y)=0。
(3)若一個局面x,其sg(x)=0,則x的所有後續局面y,sg(y)>0。


多個遊戲下有sg定理:對於多個單一遊戲,X=x[1..n],每一次我們只能改變其中一個單一遊戲的局面,則其總局面的sg值等於這些單一遊戲的sg值異或和。

即:sg(X) = sg(x[1]) xor sg(x[2]) xor … xor sg(x[n])

簡單粗暴的理解就是1堆石頭(100個)分成2堆,分別為p和q個,則sg[ 100 ] = sg[ p] ^ sg[ q ];

/*****************************上面內容是hihocoder講述的簡化,畢竟講的不錯**************************/

sg[n]的具體數值代表什麼?

sg[n] = 0 :必敗,不管怎麼走都是輸,要麼因為無法操作而直接輸或者後續狀態是必勝態,導致對手贏了
sg[n] = 1 : 一階必勝,不管怎麼走都會轉到必敗給對手
sg[n] = 2 : 二階必勝,可以走到必敗,也可以走到一階必勝,當然我們會選擇必敗給對方
sg[n] = 3 : 三階必勝,可以走到必敗或者一階、二階必勝。

同理 sg[n] = x,則n這個狀態的後續狀態有0、1、2。。。x-1,即可以選擇(操作不同)到小於x的任何狀態

為什麼需要mex{}而非其他函式?

一個狀態可以經過操作後轉到必敗、二階、三階,那麼它應該是一階還是四階?根據sg[n]可以轉到小於sg[n]的狀態這個定義來看,該狀態應該是一階的,這個求的操作就剛好對應於mex{}。

既然sg[n]非0則為必敗,我們又只需要知道是否必勝,那麼是否可以將必勝都固定為1?

在單一遊戲下是可以的,因為只關注是否必勝而不在意怎麼勝,也因此多數OJ博弈題目可以省掉個迴圈,而判斷是否必勝根據mex{}我們只需判後續狀態有無必敗點,即vis[ 0 ] 是否為true,為true表示有有某個後續狀態x的sg[x]為0,下面會有例子。

在多個遊戲的組合下是不行的,多遊戲下我們會用異或和來判斷結果,但固定值的話X階必勝和Y必勝的異或可能是必勝也可能是必敗,但固定值的話直接就為0(必敗)了。為什麼是異或而非其他操作呢?請轉看上面那個知乎連結。

假如當前狀態n是必勝點,如何求它應該做什麼操作讓對方輸?

已知當前態可以轉到必敗或者1到sg[n] - 1 階勝態,而sg值為0代表必敗,所以我們在標記後續點的時候判斷下如果某個後續狀態sg值是0,那麼當前狀態轉到那個狀態即可留必敗給對手,從而勝利。

如取石頭,允許取1,2,3個石頭的情況,ans[n]就代表為0的後續狀態,那麼取n-ans[n]個即可留必敗給對手。

    int nxt[] = {1, 2, 3};
    for (int i = 0; i < 3 && nxt[i] <= n; ++i) {
        vis[sg[n - nxt[i]]] = 1;
        if (sg[n - nxt[i]] == 0) ans[n] = n - nxt[i];
    }

sg函式的適用範圍?

符合策梅洛定理的遊戲要麼先手有必勝策略,要麼後手有必勝策略,要麼雙方有必不敗策略
策梅洛定理的規則:雙人回合制、有限步結束、資訊公開完備(玩家知道此前全部操作步驟)、無隨機因素。
而sg除以上情況外還需要遊戲沒有平局,這點也就去掉了“雙方有必不敗策略”的情況,也就是說上述條件再加個無平局即可適用sg,所以取石頭遊戲也是可以適用的。

先取完者勝問題下sg[0]初始化為0是什麼情況?

sg博弈為0的定義是無法操作為敗,對於先取完者勝的情況,0是無法操作的,應該認為是敗點,如果特意說明為勝點,則sg[0]賦值為1即可。

下面看題目

N張牌,每次可以拿2的次冪個,先取完者勝。

解:若n為先手必勝,則sg[ n ] 不為0,否則為0,顯然,sg[ 0 ] = 0(是終結狀態且先手敗),而 x 的後繼狀態即 x - 2 ^ j(即取走後剩下多少),我們遍歷所有的j,把 x - 2 ^ j 標記為1,再for迴圈在vis裡找第一個0就是。

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;

const int maxn = 1000 + 5;
bool vis[maxn];
int sg[maxn];
int getSg(int n) {
	memset (vis, false, sizeof(vis));	//對每個當前狀態都清0
	for (int i = 1; i <= n; i *= 2) {
		vis[sg[n - i]] = true;	 //標記 sg[當前狀態的所有後續狀態]
	}
	for (int i = 0; i <= n; ++i){	//mex{sg[i]}, 其中 0 <= i < n
		if(!vis[i]) return sg[n] = i;
	}
}
void work(int n) {
	memset (sg, 0, sizeof(sg));
	sg[0] = 0;					//0為必敗點
	for (int i = 1; i <= n; ++i) {
		getSg(i);
	}
}

int main() {
	int n;
	work(1000);
	while (cin >> n) {
		if (sg[n]) {
			cout << "Kiki" << endl;
		} else {
			cout << "Cici" << endl;
		}
	}
	return 0;
}

因為只需知道勝負,我們可以省個迴圈,甚至把sg陣列定義為bool

int getSg(int n) {
    memset (vis, false, sizeof(vis));
    for (int i = 1; i <= n; i *= 2) {
        vis[sg[n - i]] = true;
    }
    if (vis[0]) return sg[n] = 1;
    return sg[n] = 0;
}


取石頭遊戲:n個石頭,一次能取1-m個的情況問題

#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );

const int maxn = 1e3 + 5;
int vis[maxn], sg[maxn];
int ans[maxn];

int getSgN(int n, int m) {
    clr(vis, 0);
    for (int i = 1; i <= m && m <= n; ++i) {
        vis[sg[n - i]] = 1;
        if (sg[n - i] == 0) ans[n] = i;  //取i個石頭即可留必敗點n-i給對手,從而獲勝
    }
    for (int i = 0; i <= n; ++i) {
        if (!vis[i]) return sg[n] = i;
    }
}
int getSg(int n, int m) {
    clr(sg, 0);
    clr(ans, 0);
    sg[0] = 0;
    for (int i = 1; i <= m; ++i) {  //i<=m時,一階必勝
        sg[i] = 1; ans[i] = i;
    }
    for (int i = m + 1; i <= n; ++i) {
        getSgN(i, m);
    }
}

int main() {
    int n = 20, m = 3;{
    // while (cin >> n >> m) {
        getSg(n, m);
        for (int i = 0; i < n; ++i) {
            if (sg[i]) {
                printf("%d個石頭先手必勝,應取%d個石頭讓對手處於%d這個必敗狀態,sg值為%d\n", i, ans[i], i - ans[i],sg[i] );
            }else{
                printf("%d個石頭先手必敗,不管怎麼取,都會留必勝點給對手\n", i );
            }
            puts("");
        }
    }
    return 0;
}

HDU 1848.Fibonacci again and again

3堆石頭,解就是各堆石頭sg值的異或(SG定理),不能直接判斷為1還是0了,需要存sg值

#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define clr( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("in.txt","r",stdin);
#define WE freopen("out.txt","w",stdout);

const int maxn = 1000 + 5;
int fib[16] = {1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597};
int vis[maxn];
int sg[maxn];
int gg(int n) {
    clr(vis, 0);
    for (int i = 0; fib[i] <= n; i ++) {
        vis[sg[n - fib[i]]] = 1;
    }
    for (int i = 0; i <= n; ++i){
        if(!vis[i]) return sg[n] = i;
    }
}
void gao(int n) {
    clr(sg, 0);
    sg[0] = 0;
    for (int i = 1; i <= n; ++i) {
        gg(i);
    }
}

int main() {
    int n, m, q;
    gao(1000);
    while (cin >> n >> m >> q, n || m || q) {

        if (sg[n] ^ sg[m] ^ sg[q] ) {
            cout << "Fibo" << endl;
        } else {
            cout << "Nacci" << endl;
        }
    }
    return 0;
}


n個珠子的環形鏈,每次必須選連續的m個,然後斷開,不能操作者輸。

因為連續m個的起點和終點不同,當不夠m個時為敗,這導致了相同的n會有不同的結果,所以不能簡單的for迴圈了,得記憶化搜尋來。 

#include <cstdio>
#include <cstring>
#include <cmath>
#include <iostream>

using namespace std;

int t, n, m;
int f[1005];
bool emerge[1000];

int get_sg(int len)
{
    if(len<m) return f[len]=0;
    if(f[len]!=-1) return f[len];
    bool vs[1001]={0};
    for(int i=0; len-i-m>=0; i++)
        vs[get_sg(i)^get_sg(len-i-m)]=1;
    for(int i=0; i<1001; i++)
        if(!vs[i]) return f[len]=i;
}

int main()
{
    scanf("%d", &t);
    for(int ca=1; ca<=t; ca++)
    {
        scanf("%d%d", &n, &m);
        memset(f, -1, sizeof(f));
        printf("Case #%d: ", ca);
        if(n<m || get_sg(n-m)) printf("abcdxyzk\n");
        else printf("aekdycoin\n");
    }
    return 0;
}
/*  //迷之WA
#include <cstdio>
#include <cmath>
#include <cstring>
#include <string>
#include <set>
#include <map>
#include <stack>
#include <queue>
#include <vector>
#include <iostream>
#include <algorithm>
using namespace std;
#define ll long long
#define eps 10^(-6)
#define Q_CIN ios::sync_with_stdio(false);
#define REP( i , n ) for ( int i = 0 ; i < n ; ++ i )
#define FOR( i , a , b ) for ( int i = a ; i <= b ; ++ i )
#define CLR( a , x ) memset ( a , x , sizeof (a) );
#define RE freopen("1.in","r",stdin);
#define WE freopen("1.out","w",stdout);
int n,m,sg[1005];
bool gao(int len)
{
    if(sg[len]!=-1)    return sg[len];
    if(len<m)   return sg[len]=0;
    bool vis[1001]={0};
    for(int i=0;len-m-i>=0;i++){
        vis[ gao(i) ^ gao(len-i-m) ] = 1;
    }
    for(int i=0;i<1001;i++){
        if(!vis[i])
            return sg[len]=i;
    }
}

int main()
{
//    RE
    int t;
    cin>>t;
    for(int te=1;te<=t;te++){
        cin>>n>>m;
        memset(sg,-1,sizeof(sg));
        cout<<"Case #"<<te<<": ";
        if(n<m||gao(n-m)){
            cout<<"abcdxyzk"<<endl;
        }
        else cout<<"aekdycoin"<<endl;
    }
    return 0;
}
*/