NIM遊戲/SG函數

2023-04-02 18:00:30

NIM遊戲

先看一下一維 NIM遊戲。

有一堆大小為 \(n\) 的石子,甲和乙輪流從石堆裡面拿石子,不能一次拿掉所有石子,取走最後一個石子的人獲勝,甲先開始,誰是必勝的?

顯然,誰先手,誰就獲勝。那麼推廣到二維呢?

有兩堆大小為 \(n\) \(m\) 的石子,甲和乙輪流從兩個石堆裡拿石子,每次從一個石堆裡拿石子,不能一次拿掉一堆中所有石子,取走最後一個石子的獲勝,甲先開始拿,誰是必勝的?

\(n=m\) 的時候,先手必輸。因為先手從一堆中拿 \(Y\) 顆,後手也可以從另外一個堆中拿 \(Y\) 顆。迴圈下去,後手必勝。
而如果 \(n != m\),先手就可以製造兩堆相等的石子,使得後手必敗。

引入點新概念。當兩堆相等時,先手必敗,我們將這種狀態叫做必敗態。記為 \(P\)
當兩堆不相等時,先手必勝,將這種狀態叫做必勝態,記為 \(N\)

那麼,推廣到多維,如何確定誰是必勝的呢?
由前兩種NIM遊戲可以知道,如果所有石堆大小都相等,那麼先手是不能直接取得勝利的。這種狀態稱為平衡態。反之,可以進行一次操作就取得勝利的狀態就是非平衡態。平衡態也就是必勝態,非平衡態也就是必敗態。
考慮將所有石堆的大小互斥或起來,如果結果為 \(0\),那麼這就是一個平衡態。如果結果不為零,那就是非平衡態。
我們每拿一次石子,都可以將互斥或時某一位上的值由這位上的 \(1\) 的奇偶性決定。因此我們拿石子時可以控制每一位上 \(1\) 的奇偶性,也就因此能控制互斥或出的總結果了。同樣的,對手每操作一次,由於必須拿至少一顆石頭,就勢必將會影響某一位上 \(1\) 的數量,狀態必然會改變。這就意味著我們在操作的時候,可以實現平衡態和非平衡態之間的轉化

如果一開始我們接手的是一個不平衡態,要取勝,就可以反覆給對手構造一個平衡態,這是必勝的。
如果一開始接手的是一個平衡態,只要對手足夠聰明,就可以讓我們每次都拿到一個平衡態。這就是必敗的。

SG函數

由剛才的論述可知,必勝態和必敗態之間是可以相互轉化的。必敗態經過一次操作必然會轉化為必勝態,必勝態經過一次操作可能是必勝態,也可能是必敗態(想想互斥或的過程)。當一個狀態已經轉化為能夠分出勝負的必敗態時,稱這個狀態是0級必勝點,記作 \(SG(x)=0\)\(x\) 描述了當前的狀態)。
如果某個狀態最少操作一次就能變為0級必勝點,那麼這個狀態就是1級必勝點,以此類推,有2級,3級……必勝點。而SG函數就是用來描述每個狀態到達終末態時所需要的最少的步驟,即描述每個狀態是幾級必勝點。定義為:

\[SG(x) = MEX\{SG(y)|x->y\} \]

SG定理

  • 兩個同級必勝點(SG函數值相等)組成的遊戲是必敗的。因為先手如果降低其中一個必勝點的等級,後手可以降低另一個必勝點的相同數量的等級,使先手一直面對兩個同級必勝點,最後面對兩個1級必勝點,只能將其中一個必勝點變成必敗點,這樣先手必敗。

  • 兩個不同級必勝點(SG函數值不同)組合成的的遊戲是必勝的,因為先手可以將等級高的必勝點的等級降低到與另一個必勝點相同,這樣後手面對的就是由兩個同級必勝點構成局面,先手必勝。

對於一個遊戲,可以將組成它的每一個遊戲的SG函數值互斥或起來,為 \(0\),則對於先手來說必敗,反之對於先手就是必勝的。這就是 SG定理了!

例題:

Fibonacci again and again

三堆大小分別為 \(n\)\(m\)\(p\) 的石子,每堆大小均不超過 \(1000\),兩個人拿,令 \(x\) 為菲波那契數列中的一項,每個人每次只能從一堆裡拿 \(x\) 個石子,問誰是必勝的。

板題,主要想說說怎麼記憶化搜尋求SG函數值。

int sg[MAXN + 5],f[MAXN + 5];//f為菲波那契數列
int getsg(int num){
    if(num == 0)return sg[num] = 0;
	if(sg[num] != -1)return sg[num];
	bool vis[MAXN + 5];//表示從石子數為num可以轉換到哪些狀態
	for(int i = 1; i <= MAXN: i++){
		vis[num - f[i]] = 1;
		getsg(num - f[i]);
	}
	for(int i = 0; ; i++){
		if(!vis[i])return sg[num] = i;//找mex,求出是幾級必勝點
	}
	return sg[num];
}

求出三個數的SG值後,看 \(SG(n) ^ SG(m) ^ SG(p)\) 是否為 \(0\) 即可得出答案。

A multiplication game

給定一個 \(n\),令 \(p = 1\),甲和乙可以每次將 \(p\) 乘上一個屬於 \([2,9]\) 的數,誰使 \(p\) 大於 \(n\) 誰就贏。求誰是必勝的。

#include<iostream>
#include<cstring>
#include<map>
#define int long long
using namespace std;
const int MAXN = 1e5;
int n;
map<int,int> sg,vis;
int getsg(int x){
    if(x >= n)return sg[x] = 0;
    if(vis.find(x) != vis.end())return sg[x];
    vis[x] = 1;
    int s[10];//9的十次方已經大於n的最大值了,故sg函數的值最大為9
    memset(s,0,sizeof s);
    for(int i = 2; i <= 9; i++){
        s[getsg(x * i)] = 1;
    }
    for(int i = 0; i < 10; i++){
        if(!s[i])return sg[x] = i;
    }
    return sg[x];
}
signed main(){
    while(~scanf("%lld",&n)){
        sg.clear();
        vis.clear();
        getsg(1);
        if(sg[1])cout << "Stan wins.\n";
        else cout << "Ollie wins.\n";
    }
}

Cutting Game

兩個人在一張大小為 \(h * w\) 紙上面切,每個人每次可以橫著切一刀,也可以豎著切一刀,誰切出了 \(1 * 1\) 大小的紙,誰就獲勝,問誰必勝。

注意到狀態是二維的,即當前紙的長與寬。注意下SG函數的記錄方法。

#include<iostream>
#include<cstring>
using namespace std;
const int MAXN = 1e3;
int sg[MAXN + 5][MAXN + 6],n,m;
int getsg(int x,int y){
    if(x < y)swap(x,y);
    if(sg[x][y] != -1)return sg[x][y];
    if(x == 1 && y == 1)return sg[x][y] = 1;
    bool vis[4001];
    memset(vis,0,sizeof vis);
    for(int i = 2; i <= x - 2; i++){//橫著切
        vis[getsg(i,y) ^ getsg(x - i,y)] = 1;//每切一刀,就將當前紙分成了兩張,也就是分成了兩個子游戲,因此取互斥或的值
    }
    for(int i = 2; i <= y - 2; i++){//豎著切
        vis[getsg(x,i) ^ getsg(x,y - i)] = 1;
    }
    for(int i = 0; i <= 4000; i++){
        if(!vis[i])return sg[x][y] = i;
    }
    return sg[x][y];
}
int main(){
    memset(sg,-1,sizeof sg);
    for(int i = 2; i <= MAXN; i++){
        sg[1][i] = 0;
    }
    while(~scanf("%d%d",&n,&m)){
        int ans = getsg(n,m);
        if(ans)cout << "WIN\n";
        else cout << "LOSE\n";;
    }
}