【博弈論】組合游戲及SG函數淺析


目錄

預備知識

普通的Nim游戲

SG函數

預備知識

公平組合游戲(ICG)

若一個游戲滿足:

  • 由兩名玩家交替行動;
  • 游戲中任意時刻,合法操作集合只取決於這個局面本身;
  • 若輪到某位選手時,若該選手無合法操作,則這名選手判負;

則稱該游戲為一個公平組合游戲

Nim游戲

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

mex(minimal exdudant)函數

\(S\) 表示一個非負整數集合。定義 \(mex(S)\) 為求出不屬於集合 \(S\) 的最小非負整數的運算。
如:{ \(0,1,3\) } 對應的 \(mex值\) 就是 \(2\)

SG函數簡介

定義 \(SG(x)=mex(S)\) ,其中 \(S\)\(x\) 的后繼狀態對應的 \(SG函數值\) 的集合。

\(SG\)函數板塊對應的模板題中(見下),\(x\) 代表着該堆石子的數量。

普通的Nim游戲

題目傳送門:https://www.acwing.com/problem/content/893/
題面:給定n堆石子,兩位玩家輪流操作,每次操作可以從任意一堆石子中拿走任意數量的石子(可以拿完,但不能不拿),最后無法進行操作的人視為失敗。
問如果兩人都采用最優策略,先手是否必勝。

分析

這題的結論十分地簡潔,就是:
\(a_{1} \oplus a_{2} \oplus a_{3}...\oplus a_{n}=0\) ,則先手必負,否則先手必勝。

證明:

我們記 \(a_{1} \oplus a_{2} \oplus a_{3}...\oplus a_{n}\) 為數列a的異或和,以下簡記為異或和

先給出兩條引理:

  • \(a_{1} \oplus a_{2} \oplus a_{3}...\oplus a_{n}=x~(x>0)\) 時,必可以從一堆石子中拿走若干個石子,使得異或和\(0\)
    證明:\(x\) 的最高位(記為第 \(k\) 位)是 \(1\)\(a\) 中必然存在 \(a_i\) 滿足 \(a_i\)\(k\) 位是 \(1\) ,那么我們將 \(a_i\) 變為 \(a_i \oplus x\) (因為 \(x\not=0\),所以這樣操作一定合法),那么變換后的異或和即為 \(0\)

  • \(a_{1} \oplus a_{2} \oplus a_{3}...\oplus a_{n}=0\) 時,不存在合法操作,使得異或和仍為 \(0\)
    證明:假設將 \(a_i\) 變為 \(v\)異或和\(0\)
    \(a_{1} \oplus a_{2}... \oplus a_{i}...\oplus a_{n}=0\) ,我們將這個式子與上式 \(a_{1} \oplus a_{2}... \oplus v...\oplus a_{n}=0\) 聯立,即得 \(a_{i}\oplus v=0\),意味着 \(a_{i}=v\) ,即 \(a_{i}\) 不變,不是合法操作,故矛盾。

證明完引理后就不難了:
若輪到先手時,異或和\(0\) ,那么無論先手如何行動,后手都可以進行操作,使再次輪到先手時異或和仍為 \(0\) ,而游戲結束時異或和必然為 \(0\) ,故先手必敗。
反之(即若輪到先手時,異或和不為 \(0\) )后手必敗。

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

int main(){
	int n;
	cin>>n;
	int res=0;
	for(int i=1;i<=n;i++){
		int k; cin>>k;
		res^=k;
	}
	if(res) puts("Yes");
	else puts("No");
	return 0;
}

SG函數

利用一道模板題引入:
題目傳送門https://www.acwing.com/problem/content/description/895/
題面
給定 \(n\) 堆石子以及一個由 \(k\) 個不同正整數構成的數字集合 \(S\)

現在有兩位玩家輪流操作,每次操作可以從任意一堆石子中拿取石子,每次拿取的石子數量必須包含於集合 \(S\) ,最后無法進行操作的人視為失敗。

問如果兩人都采用最優策略,先手是否必勝。

分析

先從一堆石子分析開始:

例如:該堆石子有 \(6\) 個,每次可取 \(2\)\(3\) 個,求 \(SG(6)\)
我們可以畫出一棵樹,代表着兩人的決策樹。
image

注意到 \(SG(0)=0\)

根據 \(SG\) 函數的定義,對於決策樹上的點對應 \(SG\) 函數值為:
image


除了上面的例子外,
我們還可以自己構造一棵 \(SG\) 函數值構成的樹:
image

從中我們可以直觀地看出 \(SG\) 的兩個重要性質:

  • \(0\) 結點可以到 \(0\) 結點
  • \(0\) 結點一定不可以到非 \(0\) 結點

根據 \(SG\) 函數的性質以及游戲規則,\(SG(x)=0\) 時意味着相應的玩家必負。

分析多堆石子的情況:

我們規定,對於每堆石子 \(G_i\) ,對應的 \(SG(G_i)=SG(x)\) ,其中 \(x\) 是該堆石子最初的數量

結合這棵樹:
image
\(SG\) 函數可以看出,當先手進行決策后,對應的的 \(SG\) 函數值可以為 \([0,SG(x)-1]\),這恰好就像我們最初討論的普通的Nim問題中取石子的規則!

在這里,我們將 \(SG\) 函數值看成是普通的Nim問題中石子的數量就可以用相同的方法解決了。

\(SG\) 函數的辦法

我采取的是記憶化搜索的辦法,見下:

int f[M]; // SG函數的值
int s[N]; // 可以取多少石子

int sg(int x){
	if(f[x]!=-1) return f[x]; // 當已經更新過就直接返回。
	
	unordered_set<int> S;
	for(int i=1;i<=k;i++)
		if(x-s[i]>=0) S.insert(sg(x-s[i]));
	
	for(int i=0;;i++)
		if(!S.count(i)) return f[x]=i;
}
代碼:
#include<bits/stdc++.h>
using namespace std;

const int N=105 ,M=1e4+5;
int n,k;
int f[M]; // SG函數的值
int s[N]; // 可以取多少石子

int sg(int x){
	if(f[x]!=-1) return f[x]; // 當已經更新過就直接返回。
	
	unordered_set<int> S;
	for(int i=1;i<=k;i++)
		if(x-s[i]>=0) S.insert(sg(x-s[i]));
	
	for(int i=0;;i++)
		if(!S.count(i)) return f[x]=i;
}

int main(){
	memset(f,-1,sizeof f); // init
	
	cin>>k;
	for(int i=1;i<=k;i++) cin>>s[i];
	
	cin>>n;
	
	int res=0;
	for(int i=1;i<=n;i++){
		int t; cin>>t;
		res^=sg(t);
	}
	if(res) puts("Yes");
	else puts("No");
	
	return 0;
}


免責聲明!

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



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