博弈論之SG函數——楊子曰數學

博弈論之SG函數——楊子曰數學

超鏈接:數學合集


我終於略懂了的SG函數的皮毛!


首先,我們要知道一個運算——mex(S)

表示的就是不屬於集合S的最小的自然數

比如:mex({0,1,4,5})=2mex(\{0,1,4,5\})=2mex({2,3,5})=0mex(\{2,3,5\})=0之類的


然後你還要知道幾個博弈論中應該知道的東西:

  • 一個狀態的後繼狀態,指的是通過一個操作可以從當前狀態達到的狀態
  • 必敗態:就是當前輪到你,你無論怎麼玩都是輸,則當前的狀態是必敗態
    那怎麼判斷一個狀態是不是必敗態捏?
    就是它所有的後繼狀態都是必勝態(猜也能猜出這是啥),那麼這個狀態就是必敗態
  • 必勝態:就是輪到你,你能機智地做一些操作,使得你必贏,則當前這個狀態就是必勝態
    那怎麼判斷一個狀態是不是必勝態捏?
    就是這個狀態的後繼狀態中有一個必敗態,那麼這個狀態就是必勝態(你只需要把它弄成那個必敗態就好了)

這也就是說明,一個狀態不是必敗態,就是必勝態(就看後繼狀態裏有沒有必敗態)


然後,我們今天的主角就登場了——SG函數

你現在可能一臉懵逼(・∀・(・∀・(・∀・*),SG函數,什麼鬼?

不要緊張,你現在可以這樣理解:對於每一個遊戲局面都有一個函數來描述它的狀態,我們就管這個函數叫做SG函數

(但是你還是要注意,不同的狀態有可能有相同的SG值,往下讀,你就可以更加深刻地理解它了)

首先,告訴大家的是這樣兩個事情:

  • SG值大於0,這是一個必勝態;SG值等於0,這是一個必敗態
  • y1,y2,,yky_1,y_2, \cdots,y_k表示狀態s的後繼狀態,那麼SG(s)=mex(SG(y1),SG(y2),,SG(yk))SG(s)=mex({SG(y_1),SG(y_2),\cdots,SG(y_k)})

先不要管下面這個式子的其他疑惑(我知道你對這個式子有很多疑惑),我們就先來看一看爲什麼下面這個式子能滿足上面那個結論

首先,如果一個狀態的後繼狀態全是必勝態,那麼所有的SG(y1),SG(y2),,SG(yk)SG(y_1),SG(y_2),\cdots,SG(y_k)都是大於0的,然後對它們取mex,得到的數一定是0,也就是必敗態,滿足條件!

如果,一個狀態的後繼狀態裏,有必敗態,也就是在SG(y1),SG(y2),,SG(yk)SG(y_1),SG(y_2),\cdots,SG(y_k)中有0這個數字,那麼我們在取完mex以後得到的數一定不是0,也就是必勝態,同樣滿足條件!

完美!

那麼我們爲什麼要用這樣的式子來計算SG呢?

我們來深入理解一下:
如果一個狀態s的SG值等於a,那麼根據:
SG(s)=mex(SG(y1),SG(y2),,SG(yk))SG(s)=mex({SG(y_1),SG(y_2),\cdots,SG(y_k)})

我們可以得到的是:在集合{SG(y1),SG(y2),,SG(yk)}\{SG(y_1),SG(y_2),\cdots,SG(y_k)\}中一定有0~a-1中的每個數字

這就說明的一個事情:一個SG值等於a的狀態保證能轉移到SG值爲0~a-1的一個狀態

到這裏我們就可以用它解決一些簡單的博弈論題目了:

現在有5顆石子,兩個人輪流抓,每輪可以抓走1顆,3顆,或者4顆,抓完的人獲勝,問:先手必勝,還是必敗?

這道題就是問我們SG(5)的值

我們先來看一下邊界條件:場上剩0顆,顯然是必敗態,也就是SG(0)=0,然後我們就可以開始遞推了:
SG(1)=mex(SG(0))=1SG(1)=mex(SG(0))=1

SG(2)=mex(SG(1))=0SG(2)=mex(SG(1))=0

SG(3)=mex(SG(0),SG(2))=1SG(3)=mex(SG(0),SG(2))=1

SG(4)=mex(SG(0),SG(1),SG(3))=2SG(4)=mex(SG(0),SG(1),SG(3))=2

SG(5)=mex(SG(1),SG(2),SG(4))=3SG(5)=mex(SG(1),SG(2),SG(4))=3

So,先手必勝


SG函數的應用怎麼可能只有這麼一點呢?

我們的題目往往不會這麼單一,我們的狀態是可以由多種小狀態組成的


比如這樣一道題目(很多博客講到這裏都喜歡用經典Nim遊戲舉例子,但我覺得下面的例子更加清晰):

給定一張N×M的矩形網格紙,兩名玩家輪流行動。在每一次行動中,可以任選一張矩形網格紙,沿着某一行或者某一列的格線將之剪成兩部分。首先剪出1×1的玩家獲勝。兩名玩家都採用最優策略,問先手是否必勝?

Input Data
The input contains multiple test cases. Each test case contains only two integers W and H (2 <= W, H <= 200) in one line, which are the width and height of the original paper.

Output Data
For each test case, only one line should be printed. If the one who cut first can win the game, print “WIN”, otherwise, print “LOSE”.

Input Sample

2 2
3 2
4 2

Output Sample

LOSE
LOSE
WIN

這道題你就會發現一個狀態是可以由多種小狀態組成的

當前是一個n*m的矩陣,這是我們的母狀態,我們可以通過枚舉從哪裏切開(其實就是在枚舉後繼狀態),可以得到兩個矩陣,這兩個矩陣有分別對應兩個子狀態,那麼我們能不能根據子狀態的SG值算出母狀態是SG值捏?

當然可以,我們甚至可以得到這樣的式子:

如果x是母狀態,y1,y2,,,yky_1,y_2,\cdots,,y_k是x的子狀態,有:
SG(x)=SG(y1)SG(y2)SG(yk)SG(x)=SG(y_1)\oplus SG(y_2)\oplus \cdots\oplus SG(y_k)

神不神奇!

我們來簡單地證明一下:

要證明上面那個事情,就是要證明這樣一個東東(注意,A+B必須被分成A和B):
SG(A+B)=SG(A)SG(B)SG(A+B)=SG(A)\oplus SG(B)

我們用一個非常佛系的方法來證明這個事情:

假設這件事情是對的……

首先,由於A+B必須被分成A和B,那麼A+B之後可以達到的狀態,就是A可以達到的狀態\bigcupB可以達到的狀態

然後,假設SG(A)=1,SG(B)=2,(前面已經說了,當前狀態可以轉移到SG值小於它的SG值的狀態)那麼可能達到的狀態是SG值是02=210=111=00 \oplus 2 = 2、1 \oplus 0 = 1、1 \oplus 1 = 0
然後你有沒有發現,小於12=31 \oplus 2 = 3的值都可以表示出來

假設SG(A)=1,SG(B)=3,那麼可能達到的狀態是SG值是那麼可能達到的狀態是SG值是03=310=111=012=30 \oplus 3 = 3、1 \oplus 0 = 1、1 \oplus 1 = 0、1 \oplus 2 = 3
然後你有沒有發現,小於13=11 \oplus 3 = 1的值都可以表示出來

假設SG(A)=2,SG(B)=3,那麼可能達到的狀態是SG值是那麼可能達到的狀態是SG值是03=313=220=221=322=00 \oplus 3 = 3、1 \oplus 3 = 2、2 \oplus 0 = 2、2 \oplus 1 = 3、2 \oplus 2 = 0
然後你有沒有發現,小於23=12 \oplus 3 = 1的值都可以表示出來

機智的你一定發現了什麼驚悚的規律!

A和B這兩個狀態可以表示出所有SG值小於SG(A)SG(B)SG(A)\oplus SG(B)的至少一個狀態,也就是說SG(A)SG(B)SG(A)\oplus SG(B)就是A狀態和B狀態第一個達不到的狀態的SG值

這件事情證明對於我來說有些困難,只能找找規律了,想要知道怎麼證明可以戳這裏

然後你有沒有發現,SG值小於SG(A)SG(B)SG(A)\oplus SG(B)的狀態都是可以被我們達到的,完全符合我們SG(A+B)的定義有沒有!

就這樣,非常不嚴謹地得證了!


我們在回到上面的那題,假設我們現在拿到手的矩陣是n*m的,我們枚舉他從哪裏切開,也就是在枚舉它的後繼狀態,對於每個後繼狀態是由兩個矩陣組成的,我們可以利用上面的公式求出這個後繼狀態的SG值,在對所有後繼狀態的SG值取個mex就可以得到當前矩陣的SG值了!

那麼臨界情況是什麼捏?

顯然如果出現了1m1*m或者n1n*1這樣的矩陣的話,只要剪出一個111*1,就必勝了

然而要到這兩個情況必定途經:23,32,222*3,3*2,2*2,只有它們的後繼狀態全是形如上面那兩個矩陣的,So,它們是必敗態

而題目中的長和寬都是保證大於等於2的,So,邊界情況我們就記錄:SG[2][3]=SG[3][2]=SG[2][2]=0

OK,完事


c++代碼(POJ2311):

#include<bits/stdc++.h>
using namespace std;

const int maxn=205;

int sg[maxn][maxn];

int getsg(int n,int m){
    if (sg[n][m]!=-1) return sg[n][m];
    bool flag[maxn*2];
    memset(flag,0,sizeof(flag));
    for (int i=2;i<=n-i;i++){
        flag[getsg(i,m)^getsg(n-i,m)]=1;
    }
    for (int i=2;i<=m-i;i++){
        flag[getsg(n,i)^getsg(n,m-i)]=1;
    }
    for (int i=0;i<=300;i++){
        if (flag[i]==0){
            return sg[n][m]=i;
        }
    }
}

int main(){
    memset(sg,-1,sizeof(sg));
    sg[2][2]=sg[2][3]=sg[3][2]=0;
    int n,m;
    while(scanf("%d%d",&n,&m)!=EOF){
        if (getsg(n,m)) puts("WIN");
        else puts("LOSE");
    }
    return 0;
}

參考:
https://zhuanlan.zhihu.com/p/20611132
《算法競賽進階指南》 李煜東 著

於HG機房

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章