1. 程式人生 > >博弈問題及SG函式(真的很經典)

博弈問題及SG函式(真的很經典)

博弈問題
若你想仔細學習博弈論,我強烈推薦加利福尼亞大學的Thomas S. Ferguson教授精心撰寫並免費提供的這份教材,它使我受益太多。(如果你的英文水平不足以閱讀它,我只能說,恐怕你還沒到需要看“博弈論”的時候。)

Nim遊戲是博弈論中最經典的模型(之一?),它又有著十分簡單的規則和無比優美的結論,由這個遊戲開始瞭解博弈論恐怕是最合適不過了。

Nim遊戲是組合遊戲(Combinatorial Games)的一種,準確來說,屬於“Impartial Combinatorial Games”(以下簡稱ICG)。滿足以下條件的遊戲是ICG(可能不太嚴謹):1、有兩名選手;2、兩名選手交替對遊戲進行移動(move),每次一步,選手可以在(一般而言)有限的合法移動集合中任選一種進行移動;3、對於遊戲的任何一種可能的局面,合法的移動集合只取決於這個局面本身,不取決於輪到哪名選手操作、以前的任何操作、骰子的點數或者其它什麼因素; 4、如果輪到某名選手移動,且這個局面的合法的移動集合為空(也就是說此時無法進行移動),則這名選手負。根據這個定義,很多日常的遊戲並非ICG。例如象棋就不滿足條件3,因為紅方只能移動紅子,黑方只能移動黑子,合法的移動集合取決於輪到哪名選手操作。

通常的Nim遊戲的定義是這樣的:有若干堆石子,每堆石子的數量都是有限的,合法的移動是“選擇一堆石子並拿走若干顆(不能不拿)”,如果輪到某個人時所有的石子堆都已經被拿空了,則判負(因為他此刻沒有任何合法的移動)。

這遊戲看上去有點複雜,先從簡單情況開始研究吧。如果輪到你的時候,只剩下一堆石子,那麼此時的必勝策略肯定是把這堆石子全部拿完一顆也不給對手剩,然後對手就輸了。如果剩下兩堆不相等的石子,必勝策略是通過取多的一堆的石子將兩堆石子變得相等,以後如果對手在某一堆裡拿若干顆,你就可以在另一堆中拿同樣多的顆數,直至勝利。如果你面對的是兩堆相等的石子,那麼此時你是沒有任何必勝策略的,反而對手可以遵循上面的策略保證必勝。如果是三堆石子……好像已經很難分析了,看來我們必須要藉助一些其它好用的(最好是程式化的)分析方法了,或者說,我們最好能夠設計出一種在有必勝策略時就能找到必勝策略的演算法。

定義P-position和N-position,其中P代表Previous,N代表Next。直觀的說,上一次move的人有必勝策略的局面是P- position,也就是“後手可保證必勝”或者“先手必敗”,現在輪到move的人有必勝策略的局面是N-position,也就是“先手可保證必勝”。更嚴謹的定義是:1.無法進行任何移動的局面(也就是terminal position)是P-position;2.可以移動到P-position的局面是N-position;3.所有移動都導致N-position 的局面是P-position。

按照這個定義,如果局面不可能重現,或者說positions的集合可以進行拓撲排序,那麼每個position或者是P-position或者是N-position,而且可以通過定義計算出來。

以Nim遊戲為例來進行一下計算。比如說我剛才說當只有兩堆石子且兩堆石子數量相等時後手有必勝策略,也就是這是一個P-position,下面我們依靠定義證明一下(3,3)是一個P-position。首先(3,3)的子局面(也就是通過合法移動可以導致的局面)有(0,3)(1,3)(2,3)(顯然交換石子堆的位置不影響其性質,所以把(x,y)和(y,x)看成同一種局面),只需要計算出這三種局面的性質就可以了。 (0,3)的子局面有(0,0)、(0,1)、(0,2),其中(0,0)顯然是P-position,所以(0,3)是N-position(只要找到一個是P-position的子局面就能說明是N-position)。(1,3)的後繼中(1,1)是P-position(因為(1,1)的唯一子局面(0,1)是N-position),所以(1,3)也是N-position。同樣可以證明(2,3)是N-position。所以(3,3)的所有子局面都是N-position,它就是P-position。通過一點簡單的數學歸納,可以嚴格的證明“有兩堆石子時的局面是P-position當且僅當這兩堆石子的數目相等”。

根據上面這個過程,可以得到一個遞迴的演算法——對於當前的局面,遞迴計算它的所有子局面的性質,如果存在某個子局面是P-position,那麼向這個子局面的移動就是必勝策略。當然,可能你已經敏銳地看出有大量的重疊子問題,所以可以用DP或者記憶化搜尋的方法以提高效率。但問題是,利用這個演算法,對於某個Nim遊戲的局面(a1,a2,...,an)來說,要想判斷它的性質以及找出必勝策略,需要計算O(a1*a2*...*an)個局面的性質,不管怎樣記憶化都無法降低這個時間複雜度。所以我們需要更高效的判斷Nim遊戲的局面的性質的方法。

直接說結論好了。(Bouton's Theorem)對於一個Nim遊戲的局面(a1,a2,...,an),它是P-position當且僅當a1^a2^...^an=0,其中^表示異或(xor)運算。怎麼樣,是不是很神奇?我看到它的時候也覺得很神奇,完全沒有道理的和異或運算扯上了關係。但這個定理的證明卻也不復雜,基本上就是按照兩種position的證明來的。

根據定義,證明一種判斷position的性質的方法的正確性,只需證明三個命題: 1、這個判斷將所有terminal position判為P-position;2、根據這個判斷被判為N-position的局面一定可以移動到某個P-position;3、根據這個判斷被判為P-position的局面無法移動到某個P-position。

第一個命題顯然,terminal position只有一個,就是全0,異或仍然是0。

第二個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an!=0,一定存在某個合法的移動,將ai改變成ai'後滿足 a1^a2^...^ai'^...^an=0。不妨設a1^a2^...^an=k,則一定存在某個ai,它的二進位制表示在k的最高位上是1(否則k的最高位那個1是怎麼得到的)。這時ai^k<ai一定成立。則我們可以將ai改變成ai'=ai^k,此時 a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。

第三個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某個合法的移動,將ai改變成ai'後滿足 a1^a2^...^ai'^...^an=0。因為異或運算滿足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以將ai改變成ai'不是一個合法的移動。證畢。

根據這個定理,我們可以在O(n)的時間內判斷一個Nim的局面的性質,且如果它是N-position,也可以在O(n)的時間內找到所有的必勝策略。Nim問題就這樣基本上完美的解決了。

在下一節“Sprague-Grundy函式”中,我們將面對更多與Nim遊戲有關的變種,還會看到Nim遊戲的a1^a2^...^an這個值更廣泛的意義。敬請期待。


上一期的文章裡我們仔細研究了Nim遊戲,並且瞭解了找出必勝策略的方法。但如果把Nim的規則略加改變,你還能很快找出必勝策略嗎?比如說:有n堆石子,每次可以從第1堆石子裡取1顆、2顆或3顆,可以從第2堆石子裡取奇數顆,可以從第3堆及以後石子裡取任意顆……這時看上去問題複雜了很多,但相信你如果掌握了本節的內容,類似的千變萬化的問題都是不成問題的。

現在我們來研究一個看上去似乎更為一般的遊戲:給定一個有向無環圖和一個起始頂點上的一枚棋子,兩名選手交替的將這枚棋子沿有向邊進行移動,無法移動者判負。事實上,這個遊戲可以認為是所有Impartial Combinatorial Games的抽象模型。也就是說,任何一個ICG都可以通過把每個局面看成一個頂點,對每個局面和它的子局面連一條有向邊來抽象成這個“有向圖遊戲”。下面我們就在有向無環圖的頂點上定義Sprague-Garundy函式。

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

對於一個給定的有向無環圖,定義關於圖的每個頂點的Sprague-Garundy函式g如下:g(x)=mex{ g(y) | y是x的後繼 }。

來看一下SG函式的性質。首先,所有的terminal position所對應的頂點,也就是沒有出邊的頂點,其SG值為0,因為它的後繼集合是空集。然後對於一個g(x)=0的頂點x,它的所有後繼y都滿足 g(y)!=0。對於一個g(x)!=0的頂點,必定存在一個後繼y滿足g(y)=0。

以上這三句話表明,頂點x所代表的postion是P-position當且僅當g(x)=0(跟P-positioin/N-position的定義的那三句話是完全對應的)。我們通過計算有向無環圖的每個頂點的SG值,就可以對每種局面找到必勝策略了。但SG函式的用途遠沒有這樣簡單。如果將有向圖遊戲變複雜一點,比如說,有向圖上並不是只有一枚棋子,而是有n枚棋子,每次可以任選一顆進行移動,這時,怎樣找到必勝策略呢?

讓我們再來考慮一下頂點的SG值的意義。當g(x)=k時,表明對於任意一個0<=i<k,都存在x的一個後繼y滿足g(y)=i。也就是說,當某枚棋子的SG值是k時,我們可以把它變成0、變成1、……、變成k-1,但絕對不能保持k不變。不知道你能不能根據這個聯想到Nim遊戲,Nim 遊戲的規則就是:每次選擇一堆數量為k的石子,可以把它變成0、變成1、……、變成k-1,但絕對不能保持k不變。這表明,如果將n枚棋子所在的頂點的 SG值看作n堆相應數量的石子,那麼這個Nim遊戲的每個必勝策略都對應於原來這n枚棋子的必勝策略!

對於n個棋子,設它們對應的頂點的SG值分別為(a1,a2,...,an),再設局面(a1,a2,...,an)時的Nim遊戲的一種必勝策略是把 ai變成k,那麼原遊戲的一種必勝策略就是把第i枚棋子移動到一個SG值為k的頂點。這聽上去有點過於神奇——怎麼繞了一圈又回到Nim遊戲上了。

其實我們還是隻要證明這種多棋子的有向圖遊戲的局面是P-position當且僅當所有棋子所在的位置的SG函式的異或為0。這個證明與上節的Bouton's Theorem幾乎是完全相同的,只需要適當的改幾個名詞就行了。

剛才,我為了使問題看上去更容易一些,認為n枚棋子是在一個有向圖上移動。但如果不是在一個有向圖上,而是每個棋子在一個有向圖上,每次可以任選一個棋子(也就是任選一個有向圖)進行移動,這樣也不會給結論帶來任何變化。

所以我們可以定義有向圖遊戲的和(Sum of Graph Games):設G1、G2、……、Gn是n個有向圖遊戲,定義遊戲G是G1、G2、……、Gn的和(Sum),遊戲G的移動規則是:任選一個子遊戲Gi 並移動上面的棋子。Sprague-Grundy Theorem就是:g(G)=g(G1)^g(G2)^...^g(Gn)。也就是說,遊戲的和的SG函式值是它的所有子游戲的SG函式值的異或。

再考慮在本文一開頭的一句話:任何一個ICG都可以抽象成一個有向圖遊戲。所以“SG函式”和“遊戲的和”的概念就不是侷限於有向圖遊戲。我們給每個 ICG的每個position定義SG值,也可以定義n個ICG的和。所以說當我們面對由n個遊戲組合成的一個遊戲時,只需對於每個遊戲找出求它的每個局面的SG值的方法,就可以把這些SG值全部看成Nim的石子堆,然後依照找Nim的必勝策略的方法來找這個遊戲的必勝策略了!

回到本文開頭的問題。有n堆石子,每次可以從第1堆石子裡取1顆、2顆或3顆,可以從第2堆石子裡取奇數顆,可以從第3堆及以後石子裡取任意顆……我們可以把它看作3個子遊戲,第1個子遊戲只有一堆石子,每次可以取1、2、3顆,很容易看出x顆石子的局面的SG值是x%4。第2個子遊戲也是隻有一堆石子,每次可以取奇數顆,經過簡單的畫圖可以知道這個遊戲有x顆石子時的SG值是x%2。第3個遊戲有n-2堆石子,就是一個Nim遊戲。對於原遊戲的每個局面,把三個子游戲的SG值異或一下就得到了整個遊戲的SG值,然後就可以根據這個SG值判斷是否有必勝策略以及做出決策了。其實看作3個子遊戲還是保守了些,乾脆看作n個子遊戲,其中第1、2個子遊戲如上所述,第3個及以後的子游戲都是“1堆石子,每次取幾顆都可以”,稱為“任取石子游戲”,這個超簡單的遊戲有x顆石子的SG值顯然就是x。其實,n堆石子的Nim遊戲本身不就是n個“任取石子游戲”的和嗎?

所以,對於我們來說,SG函式與“遊戲的和”的概念不是讓我們去組合、製造稀奇古怪的遊戲,而是把遇到的看上去有些複雜的遊戲試圖分成若干個子游戲,對於每個比原遊戲簡化很多的子游戲找出它的SG函式,然後全部異或起來就得到了原遊戲的SG函式,就可以解決原遊戲了。



例子程式:
acm.hdu上的例子

A New Tetris Game 22222

#include <iostream>
#include <set>
#include <bitset>
#include <map>
//#include <algorithm>
using namespace std;

//better if find area that not linked and sg each
typedef bitset<32> int_set;
struct array2{
    bitset<64> bs;
    int r,c;
    bool operator<(const array2& a2)const{
        if(r!=a2.r)return r<a2.r;
        if(c!=a2.c)return c<a2.c;
        return *(long long*)&bs< *(long long*)&a2.bs;
    }
    bitset<64>::reference operator()(int ir,int ic){
        return bs[ir*c+ic];
    }
}g;
inline int mex(const int_set& is){
    if(is.none())return 0;
    for(int i=0;i!=32;++i){ //assum i will never exceed 32
        if(is.test(i)==0)
            return i;
    }
}
inline bool check_ok(int ir,int ic){
    return !g(ir,ic)&&!g(ir+1,ic)&&!g(ir,ic+1)&&!g(ir+1,ic+1);
}
map<array2,int> ma;
inline void set_it(int ir,int ic,int s){
    g(ir,ic)=s;
    g(ir+1,ic)=s;
    g(ir,ic+1)=s;
    g(ir+1,ic+1)=s;
}
inline int sg(int r,int c){
    map<array2,int>::iterator p=ma.find(g);//optimizing for the repeated
    if(p!=ma.end())return p->second;
    int_set sgs;
    for(int i=0;i<r-1;++i)
        for(int j=0;j<c-1;++j)
            if(check_ok(i,j)){
                set_it(i,j,1);
                sgs.set(sg(r,c));
                set_it(i,j,0);
            }
    return ma[g]=mex(sgs);
}
int main(){
    int n;
    while(cin>>n){
        int out=0;
        while(n--){
            int r,c;
            cin>>g.r >>g.c;
            for(int i=0;i!=g.r;++i)
                for(int j=0;j!=g.c;++j){
                    char c;
                    cin>>c;
                   g(i,j)=c-'0';
                }
                out^=sg(g.r,g.c);
         }
        cout<<(out?"Yes":"No")<<endl;
    }
    return 0;
}

(轉載:原作者不詳)