1. 程式人生 > >【博弈論】組合遊戲及SG函式淺析

【博弈論】組合遊戲及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} \bigoplus a_{2} \bigoplus a_{3}...\bigoplus a_{n}=0$ ,則先手必負,否則先手必勝。 #### 證明: > 我們記 $a_{1} \bigoplus a_{2} \bigoplus a_{3}...\bigoplus a_{n}$ 為數列a的**異或和**,以下簡記為**異或和**。 先給出兩條引理: + $a_{1} \bigoplus a_{2} \bigoplus a_{3}...\bigoplus a_{n}=x~(x>0)$ 時,必可以從一堆石子中拿走若干個石子,使得**異或和**為 $0$。 證明:$x$ 的最高位(記為第 $k$ 位)是 $1$ ,$a$ 中必然存在 $a_i$ 滿足 $a_i$ 第 $k$ 位是 $1$ ,那麼我們將 $a_i$ 變為 $a_i \bigoplus x$ (因為 $x\not=0$,所以這樣操作一定合法),那麼變換後的**異或和**即為 $0$ 。 + $a_{1} \bigoplus a_{2} \bigoplus a_{3}...\bigoplus a_{n}=0$ 時,不存在合法操作,使得**異或和**仍為 $0$。 證明:假設將 $a_i$ 變為 $v$ 後**異或和**為 $0$ 即 $a_{1} \bigoplus a_{2}... \bigoplus a_{i}...\bigoplus a_{n}=0$ ,我們將這個式子與上式 $a_{1} \bigoplus a_{2}... \bigoplus v...\bigoplus a_{n}=0$ 聯立,即得 $a_{i}\bigoplus v=0$,意味著 $a_{i}=v$ ,即 $a_{i}$ 不變,不是合法操作,故矛盾。 證明完引理後就不難了: 若輪到先手時,**異或和**為 $0$ ,那麼無論先手如何行動,後手都可以進行操作,使再次輪到先手時**異或和**仍為 $0$ ,而遊戲結束時異或和必然為 $0$ ,故先手必敗。 反之(即若輪到先手時,**異或和**不為 $0$ )後手必敗。
程式碼:
```cpp #include 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](https://img2020.cnblogs.com/blog/2185228/202103/2185228-20210317194942926-1272163860.png) >
注意到 $SG(0)=0$ 根據 $SG$ 函式的定義,對於決策樹上的點對應 $SG$ 函式值為: ![image](https://img2020.cnblogs.com/blog/2185228/202103/2185228-20210317195119522-1676737765.png) 我們還可以自己構造一棵 $SG$ 函式值構成的樹: ![image](https://img2020.cnblogs.com/blog/2185228/202103/2185228-20210317201413246-1947907303.png) 從中我們可以直觀地看出 $SG$ 的兩個重要性質: + 非 $0$ 結點可以到 $0$ 結點 + $0$ 結點一定不可以到非 $0$ 結點 根據 $SG$ 函式的性質以及遊戲規則,$SG(x)=0$ 時意味著相應的玩家必負。 #### 分析多堆石子的情況: >
我們規定,對於每堆石子 $G_i$ ,對應的 $SG(G_i)=SG(x)$ ,其中 $x$ 是該堆石子**最初的數量**。 結合這棵樹: ![image](https://img2020.cnblogs.com/blog/2185228/202103/2185228-20210317201413246-1947907303.png) 從 $SG$ 函式可以看出,當先手進行決策後,對應的的 $SG$ 函式值可以為 $[0,SG(x)-1]$,這恰好就像我們最初討論的**普通的Nim問題**中取石子的規則! 在這裡,我們將 $SG$ 函式值看成是**普通的Nim問題**中石子的數量就可以用相同的方法解決了。 #### 求 $SG$ 函式的辦法 我採取的是記憶化搜尋的辦法,見下: ```cpp int f[M]; // SG函式的值 int s[N]; // 可以取多少石子 int sg(int x){ if(f[x]!=-1) return f[x]; // 當已經更新過就直接返回。 unordered_set 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; } ```
程式碼: ```cpp #include 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 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; } ```