博弈論基礎之sg函式與nim

水拌青椒發表於2019-07-24

  在演算法競賽中,博弈論題目往往是以icg。通俗的說就是兩人交替操作,每步都各自合法,合法性與選手無關,只與遊戲有關。往往我們需要求解在某一個遊戲或幾個遊戲中的某個狀態下,先手或後手誰會勝利的問題。就比如經典的:幾堆石子,兩人可以分別拿若干個,一次只能選擇一個石子堆操作,問給定狀態下,先手勝利還是後手勝利?

  而nim與sg函式就是對於這類問題的解法,在我的理解看來,sg函式和nim分別對應不同階段的決策:前者對於單個遊戲決策,後著是將這些單個遊戲綜合起來的整體決策

一、狀態與轉移

  icg遊戲往往可以分為兩個部分,規則與局面。而這兩個分別對應了轉移與狀態。規則決定了狀態轉移的合法性,狀態是轉移的基本。

  什麼是狀態?狀態是一個靜態的局面。就好比當下棋時的局面一樣。在遊戲的每個階段,不論是開始,中間,或是結束。每一個局面都對應了一種狀態。

  什麼是狀態的轉移?單個分開的局面無法構成一個完整的遊戲,所以就需要從某一個狀態轉移到另一個狀態,來進行一次操作。

  舉個例子:有5個石子放在一堆。

        5個石子就是一種狀態,在不受限制下,你可以改變這個狀態。

        例如:取走4個石子。就是將5個石子這個狀態轉移到1個石子這個狀態,操作就是取走4個石子。

        而這個操作的合法性取決於遊戲的規則。

        例如:一次最多取3個石子。 那麼上條操作取4個石子就是一次不合法的操作,意味著你不能從5這個狀態直接轉移到1這個狀態。

      那麼對於5個石子,每次最多取三個,從中我們可以得到如下狀態轉移的圖(有向)

          

二、sg函式

   概念

   首先,引入mex值的概念。mex是指不屬於集合中整數的最小正整數。而sg值就是不屬於後繼集合的最小正整數。

   例如上圖中:0沒有後繼元素  所以最小正整數為0,sg(0)=0;

         1後繼元素只有0,不屬於後繼集合的最小正整數為1,sg(1)=1;

         同理可得sg(2)=2;sg(3)=3;

         到4的時候,情況就發生了變化。由於4不能直接轉移到1,故後繼集合只有{1,2,3},sg(4)=0;

   這裡的狀態用1,2,3,4之類是為了舉例,而實際上狀態不一定是這樣轉換,可能有很多種狀態,不僅僅侷限於單個數字,亦可以是某種局面,某個棋盤局面,某個路徑局面,如果能找到”狀態“只要這個遊戲沒有迴圈,在有限步數可以達到結果,就可以對這個遊戲的每個狀態求出sg值。

   求解

  求解順序是這樣的。首先找到所有的結尾局面。比如取石子游戲中的石子剩餘為0,或是棋盤遊戲中棋子的無路可走,所以這些狀態的sg值都為0,然後從這個狀態逆向求他的前驅,求出所有前驅的sg值。 如果瞭解過拓撲排序,那麼很容易理解這個過程。用拓撲排序來講就是一個狀態的後繼sg值沒有確定時,這個狀態的sg值也無法確定。故從所有無路可走的局面按照逆向的拓撲排序過程就可以求出所有可達狀態的sg值。

   意義

  求出了sg值後,對於解這個遊戲勝負關係有什麼用呢?

  由上面的概念我們可以得到兩個結論

  1.  sg值為0的狀態,前一狀態一定全是非0狀態。(一定全是非0狀態)
  2.     sg值為非零的狀態,一定能一步轉移到0狀態(不一定必須轉到,是可以轉到)

  由此我們可以進行決策。

  如果我們先手是非零,我們可以始終選擇一步轉移到sg值為0的狀態,那麼下一家一定只能轉移到非0。那麼到你又可以一步轉移到0。迴圈如此決策,就可以讓下一家最終轉移到敗態的0狀態處,從而獲得勝利

  如果我們先手是零,那麼我們只能轉移到非0狀態,下一家可以用以上必勝決策進行操作,那麼我們就必敗。

  由此我們得到sg值與勝利與失敗的關係。

  sg(此狀態)=0時,先手的人必敗。即(此狀態)為必敗態

  sg(此狀態)≠0時,先手的人必勝。即(此狀態)為必敗態

  

  sg函式可以看做在這個遊戲下規則的體現,可以直接反映出轉移的方式。而sg()函式某個值可以視作某個狀態下的勝負情況。

  往往一個遊戲需要求的只是一個局面下的勝負情況。即一個sg(a),但實際運用中需要通過求出每個中間態的sg值來推出所需狀態的sg值。是不是有點動態規劃的思想?

三、nim博弈

  然而,一個遊戲可能由多個狀態共同構成,即兩個狀態間不能互相影響或轉移到同一個末狀態。這時單純的sg函式就不夠解題了。因此我們引入了一個多遊戲間的決策,nim博弈。對於多個遊戲間的博弈,不能用簡單的sg函式表達。可以把這個複合的遊戲進行轉變,成為多個互不影響的遊戲,即每個遊戲可以各自求出各自的sg函式,解出各自狀態對應的sg值。將多個遊戲+狀態的sg值綜合起來的方式,即為nim。

  求解方式是res=sg[1]^sg[2]^sg[3]^...^sg[n]。即為這些遊戲組合在一起後,整體的勝負態。

  右方sg值對應的是那個遊戲的起始狀態的sg值。

  (sg補充)sg值不用單純的0和非0來表示的原因:

     多個遊戲中,比如兩個遊戲,一個是必勝態,一個是必敗態。如果按照單個遊戲都要必勝的策略玩在一個遊戲結束時,再玩下一個遊戲,相當於先後手對調,先手必敗。但是先手可以將某一個的狀態從非零依舊轉移到非零,從而改變整體勝負態,比如從sg=2轉移到sg=1,對手無法再扭轉回來,自己就可以獲勝。但是如果sg更大,那麼雙方會持續做這個抉擇。這個抉擇是有盡頭的,到這個盡頭時的狀態就決定了最後整體勝負態,這個決策可執行的次數就是sg值的數量,比如sg=5時,最多可以多轉移4次(然而轉移四次不是一定最佳選擇,讀者可以進行模擬思考) 因此sg值要取具體值,在異或的時候各自的資訊也提供了用處。

  nim的決策公式推出的多個遊戲組合後的值,就對應了整體的勝負態。

四、例題

   HDU-1524 A-chess-game

  兩者綜合運用多個棋子分別看待,對應兩個獨立的遊戲,拓撲排序,然後求出每個局面打表求出對應的sg值。

  由於在同一套規則上,所以可以用同一個sg函式。將所有起始局面的sg值異或起來,根據是否為0求出結果

  程式碼如下

  

 1 #include<iostream>
 2 #include<vector>
 3 #include<cstring>
 4 using namespace std;
 5 
 6 struct point{
 7     int sg=0;
 8     vector <int> out;
 9     vector <int> in;
10     int inn=0;
11     int sgg=0;
12 }graph[10005];
13 bool num[100005];
14 int mex(int x)
15 {
16     memset(num,0,sizeof(num));
17     for(int i=0;i<graph[x].in.size();i++){
18         num[graph[graph[x].in[i]].sg]=1;
19     }
20     for(int i=0;i<10005;i++){
21         if(num[i]==0)return i;
22     }
23 }
24 void get_sg(int n)
25 {
26     for(int i=0;i<n;i++){
27         for(int i=0;i<n;i++){
28             if(graph[i].inn==0&&graph[i].sgg==0){
29                 graph[i].sg=mex(i);
30                 graph[i].sgg=1;
31                 for(int j=0;j<graph[i].out.size();j++){
32                     graph[graph[i].out[j]].inn--;
33                 }
34             }
35         }
36     }
37 }
38 int main()
39 {
40     int n,m,a;
41     while(cin>>n){
42         for(int i=0;i<=n;i++){
43             graph[i].out.clear();
44             graph[i].in.clear();
45             graph[i].sg=0;
46             graph[i].inn=0;
47             graph[i].sgg=0;
48         }
49         for(int i=0;i<n;i++){
50             cin>>m;
51             for(int j=0;j<m;j++){
52                 cin>>a;
53                 graph[i].in.push_back(a);
54                 graph[a].out.push_back(i);
55                 graph[i].inn++;
56             }
57         }
58         get_sg(n);
59         int res=0;
60         while(cin>>m){
61             if(m==0)break;
62             res=0;
63             for(int i=0;i<m;i++){
64                 cin>>a;
65                 res^=graph[a].sg;
66             }
67             if(res!=0)cout<<"WIN"<<endl;
68             else cout<<"LOSE"<<endl;
69         }
70     }
71 }
View Code

  POJ-2960 S-Nim

  從0開始,對逆向根據規則對每個狀態打表,求出sg值(如果某個點求過了,就不要重複求),再根據nim將多個結果異或起來

 1 #include<iostream>
 2 #include<cstring>
 3 using namespace std;
 4 int sg[10005];
 5 int s[10005];
 6 bool num[10005];
 7 int n,m;
 8 int mex(int x){
 9     memset(num,0,sizeof(num));
10     for(int i=0;i<n;i++){
11         if(x-s[i]>=0){
12             num[sg[x-s[i]]]=1;
13         }
14     }
15     for(int i=0;i<10005;i++){
16         if(num[i]==0)return i;
17     }
18     return -1;
19 }
20 int get_sg(){
21     for(int i=0;i<10005;i++){
22         sg[i]=mex(i);
23     }
24 }
25 int main()
26 {
27     int T ;
28     int p,q;
29     while (cin >> n) {
30         memset(sg,0,sizeof(sg));
31         if (n == 0) break;
32         for (int i = 0; i < n; i ++)cin >> s[i];
33         get_sg();
34         cin >> m;
35 
36         while (m--){
37             int res = 0;
38             cin>>q;
39             for(int i=0;i<q;i++){
40                 cin >> p;
41                 res ^= sg[p];
42             }
43             if(res==0)cout<<"L";
44             else cout<<"W";
45         }
46         cout<<endl;
47     }
48     return 0;
49 }
View Code

  

 

如有疑問或異議,歡迎私信博主進行討論與交流

  

 

相關文章