acm博弈論基礎總結
常見博弈結論
Nim
問題:共有N堆石子,編號1..n,第i堆中有個a[i]個石子。
每一次操作Alice和Bob可以從任意一堆石子中取出任意數量的石子,至少取一顆,至多取出這一堆剩下的所有石子。
結論:對於一個局面,當且僅當a[1] xor a[2] xor ...xor a[n]=0時,該局面爲P局面,即必敗局面。
證明:二進制位證明即可。
Moore’s Nim
問題:n堆石子,每次從不超過k堆中取任意多個石子,最後不能取的人失敗。
結論:這是一個nim遊戲的變形:把n堆石子的石子數用二進制表示,統計每個二進制位上1的個數,若每一位上1的個數mod(k+1)全部爲0,則必敗,否則必勝。(先手)
證明:分類討論N/P狀態。
Staircase Nim
問題:在階梯上進行,每層有若干個石子,每次可以選擇任意層的任意個石子將其移動到該層的下一層。最後不能操作的人輸。
結論:在奇數堆的石子做Nim。
證明:階梯博弈經過轉換可以變爲Nim.把所有奇數階梯看成N堆石子做nim。把石子從奇數堆移動到偶數堆可以理解爲拿走石子,就相當於幾個奇數堆的石子在做Nim。
New Nim
問題:在第一個回合中,第一個遊戲者可以直接拿走若干個整堆的火柴。可以一堆都不拿,但不可以全部拿走。第二回合也一樣,第二個遊戲者也有這樣一次機會。從第三個回合(又輪到第一個遊戲者)開始,規則和Nim遊戲一樣。如果你先拿,怎樣才能保證獲勝?如果可以獲勝的話,還要讓第一回合拿的火柴總數儘量小。
結論:爲使後手必敗,先手留給後手的必然是若干線性無關的數字,否則後手可以留下一個異或和爲零的非空子集使得先手必敗,故問題轉化爲拿走和最小的數字使得留下的數線性無關,即留下和最大的線性基,這樣拿走的數量顯然最少,找到和最大的線性基只需貪心的把數字從大到小加入到基中即可.
證明:證明需用到擬陣,但結論的正確性是比較顯然的.
Anti-Nim
問題:正常的nim遊戲是取走最後一顆的人獲勝,而反nim遊戲是取走最後一顆的人輸。
結論:一個狀態爲必勝態,當且僅當:
1)所有堆的石子個數爲1,且NIM_sum(xor和)=0
2)至少有一堆的石子個數大於1,且 NIM_sum(xor和) ≠ 0
Lasker’s Nim
問題:Alice和Bob輪流取石子,每一次可以從任意一堆中拿走任意個石子,也可以將一堆石子分爲兩個小堆。先拿完者獲勝。
結論:if(x%4==0) sg[x]=x-1;if(x%4==1||x%4==2) sg[x]=x;if(x%4==3) sg[x] = x+1。這種問題一般要對sg值打表。(對於先拿完者獲勝:sg值爲零先手必敗,否則後手必勝)
Wythoff's game
問題:兩堆石子,每次可以取一堆或兩堆,從兩堆中取得時候個數必須相同,先取完的獲勝。
結論: ak =[k *(1+√5)/2],bk= ak + k (k=0,1,2,…,n 方括號表示取整函數)
證明:
這種情況下是頗爲複雜的。我們用(ak,bk)(ak ≤ bk ,k=0,1,2,…,n)表示兩堆物品的數量並稱其爲局勢,如果甲面對(0,0),那麼甲已經輸了,這種局勢我們稱爲奇異局勢。前幾個奇異局勢是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。
可以看出,a0=b0=0,ak是未在前面出現過的最小自然數,而 bk= ak + k,奇異局勢有
如下三條性質:
1.任何自然數都包含在一個且僅有一個奇異局勢中。
由於ak是未在前面出現過的最小自然數,所以有ak > ak-1 ,而 bk= ak + k > ak-1 + k-1 = bk-1 > ak-1 。所以性質1。成立。
2.任意操作都可將奇異局勢變爲非奇異局勢。
事實上,若只改變奇異局勢(ak,bk)的某一個分量,那麼另一個分量不可能在其他奇異局勢中,所以必然是非奇異局勢。如果使(ak,bk)的兩個分量同時減少,則由於其差不變,且不可能是其他奇異局勢的差,因此也是非奇異局勢。
3.採用適當的方法,可以將非奇異局勢變爲奇異局勢。
從如上性質可知,兩個人如果都採用正確操作,那麼面對非奇異局勢,先拿者必勝;反之,則後拿者取勝。
4.(Betty 定理):如果存在正無理數 A, B 滿足 1/A + 1/B = 1,那麼集合 P = { [At], t ∈ Z+}、Q = { [Bt], t ∈ Z+} 恰爲集合 Z+ 的一個劃分,即:P ∪ Q = Z+,P ∩ Q = ø。
5.上述矩陣中每一行第一列的數爲 [Φi],第二列的數爲 [(Φ + 1)i],其中 Φ = (sqrt(5) + 1) / 2 爲黃金分割比。
Bash Game
問題:只有一堆石子共n個。每次從最少取1個,最多取m個,最後取光的人取勝。
結論:如果n=(m+1)*k+s (s!=0) 那麼先手一定必勝。
證明:因爲第一次取走s個,接下來無論對手怎麼取,我們都能保證取到所有(m+1)倍數的點,那麼循環下去一定能取到最後一個。
Sprague-Grundy Theorem
Sprague-Grundy Theorem:
SG(x)=mex{SG(y) | y是x的後繼}{SG(x):表示當前拿多少爲必勝}
g(G)=g(G1)^g(G2)^...^g(Gn)。也就是說,遊戲的和的SG函數值是它的所有子游戲的SG函數值的異或。
Fibonacci’s Game
任何正整數可以表示爲若干個不連續的Fibonacci數之和。
問題:有一堆個數爲n的石子,遊戲雙方輪流取石子,滿足:
1)先手不能在第一次把所有的石子取完;
2)之後每次可以取的石子數介於1到對手剛取的石子數的2倍之間(包含1和對手剛取的石子數的2倍)。
結論:先手勝當且僅當n不是Fibonacci數。換句話說,必敗態構成Fibonacci數列。
Game On The Tree
樹鏈博弈:
給定一棵 n 個點的樹,其中 1 號結點是根,每個結點要麼是黑色要麼是白色
現在小 Bo 和小 Biao 要進行博弈,他們兩輪流操作,每次選擇一個黑色的結點將它變白,之後可以選擇任意多個(可以不選)該點的祖先(不包含自己),然後將這些點的顏色翻轉,不能進行操作的人輸
由於小 Bo 猜拳經常輸給小 Biao,他想在這個遊戲上扳回一城,現在他想問你給定了一個初始局面,是先手必勝還是後手必勝。
題解:每層的黑點數爲偶數的時候,爲先手必敗態。首先,沒有黑點,都是0,肯定是先手必敗態。
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int maxn=1010; 5 int n,w[maxn],cnt[maxn]; 6 vector<int> g[maxn]; 7 void dfs(int u,int fa,int dep) 8 { 9 if(w[u]) cnt[dep]++; 10 for(int i=0,len=g[u].size();i<len;++i) 11 { 12 int v=g[u][i]; 13 if(v==fa) continue; 14 dfs(v,u,dep+1); 15 } 16 } 17 int main() 18 { 19 scanf("%d",&n); 20 for(int i=1;i<=n;++i) scanf("%d",w+i); 21 for(int i=1;i<n;++i) 22 { 23 int u,v; 24 scanf("%d%d",&u,&v); 25 g[u].push_back(v); 26 g[v].push_back(u); 27 } 28 dfs(1,0,1); 29 bool flag=false; 30 for(int i=1;i<=n;++i) 31 if(cnt[i]&1) flag=true; 32 if(flag) puts("First"); 33 else puts("Second"); 34 return 0; 35 }
Bamboo Stalks
作爲GH遊戲的介紹,我們先研究下面的Figure 6.1。n條線段的bamboo stalks遊戲是具有n條邊的線形圖。一步合法的操作是移除任意一條邊,玩家輪流進行操作,最後一個進行操作的玩家獲勝。n條線段的bamboo stalks遊戲能夠移動到任意更小線段數(0到n-1)的bamboo stalks遊戲局面當中。所以n條線段的bamboo stalks遊戲是等同於nim遊戲中其中擁有n個石子的一堆。玩一組bamboo stalks遊戲就相當於玩nim遊戲。
例如,左邊的三根竹竿構成的“森林”相當於具有石子數分別爲3、4、5三堆石子的nim遊戲。就我們所知,3^4^5=2,這是一個能夠移動到P局面的N局面,辦法是通過取走三根線段的竹竿上的第二根線段,留下一根。而結果變成右邊的竹竿分佈,而此時的SG值是0,是P局面。
Green Hackenbush on Trees
Colon Principle:當樹枝在一個頂點上時,用一個非樹枝的杆的長度來替代,相當於他們的n異或之和。
通過bamboo stalks遊戲,我們知道GH遊戲是換了個形式的nim遊戲而已。可是如果我們允許比bamboo stalks遊戲更多結構呢?讓我們看下在Figure 6.2中由三棵根樹組成的森林。根樹是一種圖,帶有一個最高的節點,叫做根,這個根到任意一個其他節點的路徑都是獨一無二的。實質上就是說這個圖不含有圈。
一次合法操作是移除任意一條不與地面相連的線段,此時次線段以上的子樹都會被移除。既然這個遊戲是公平的,而且根據我們學過的nim遊戲,這樣的樹相當於nim遊戲的一些堆,或者說是bamboo stalks遊戲(到了這裏明白bamboo stalks遊戲與nim遊戲的單堆是等價的)。這個問題就是尋找每棵樹的SG值。
這裏我們要用上一個原理,叫做Colon Principle:當樹枝在一個頂點上時,用一個非樹枝的杆的長度來替代,相當於他們的n異或之和。
讓我們看看這條原理是如何在Figure 6.2中尋找與左樹等價的竹竿。這裏有兩個節點擁有樹枝。較高的那個節點擁有兩個樹枝,每個樹枝上有兩個節點。1^1=0,所以這兩個樹枝可以用一個帶有0個節點樹枝來代替,也就是說可以把這兩個樹枝給去掉。那就剩下了一個Y形樹了,因爲同樣道理這個Y形樹的兩個樹枝也要被去掉。此時就剩下了一個線段數爲1的竹竿遊戲模型了。
看Figure 6.3,是Figure 6.2中第二棵樹的處理辦法,最後可以得到線段數爲8的竹竿遊戲。第三棵樹也可以同樣處理,結果是線段數爲4的竹竿遊戲。(注意,這裏所指的竹竿遊戲都實質上是nim遊戲中的單堆石子)
現在我們可以計算一下圖6.2三棵樹的sg值,也就是1^8^4=13.既然這個SG值不爲0,那麼就是一個N局面,先手必勝。問題是要怎麼找到勝利方法。很明顯這裏有一個必勝移動,通過將第二棵樹進行操作使得它的SG值爲5.可是我們要找哪條邊呢?
Figure 6.3的最後一個樹長度是8,因爲它的前一棵樹的三個樹枝長度分別是3,2,6,異或值爲3^2^6=7,用長度爲7的竹竿代替三個樹枝後,樹的長度就是根加上竹竿長度,即1+7=8。爲了最後SG達到5,即使樹的長度爲5,我們要用長度爲4的竹竿來替代那三個樹枝。因爲2^6=4,所以我們只要將最左邊的樹枝去掉就行了,當然我們也可以將樹枝改動成3^1^6=4.
修剪樹的方法用冒號給出了,把所有的樹化簡爲一個單一的竹竿。一個從最高的樹枝開始,然後用原理歸納往下到根部。我們現在展示這個原理對於含圈和多重根邊的圖同樣適用。
變形一:邊權大於1
LightOJ1355 題目大意:
給一棵帶邊權的樹,兩個人分別給邊塗色。邊權代表了這條邊可以塗色的次數。如果一條邊的塗色次數沒有用完,那麼可以塗他子樹的邊。無法塗色者爲負。
題解:green博弈變形,對於都是1的就是green博弈SG[u]^=SG[v]+1;
對於大於1的邊,偶數對其沒有貢獻,奇數有貢獻,SG[u]^= SG[v]^(val[v]%2);
1 #include<bits/stdc++.h> 2 using namespace std; 3 #define RI register int 4 #define clr(a,val) memset(a,val,sizeof(a)) 5 typedef long long ll; 6 struct Edge{ 7 int to,val,nxt; 8 } edge[2010]; 9 int x,y,z; 10 int T,n,sum1,sum2,cnt; 11 int head[2010],SG[2010]; 12 inline void addedge(int u,int v,int w) 13 { 14 edge[cnt].to=v; 15 edge[cnt].val=w; 16 edge[cnt].nxt=head[u]; 17 head[u]=cnt++; 18 } 19 inline void dfs(int u,int fa) 20 { 21 SG[u]=0; 22 for(int e=head[u];~e;e=edge[e].nxt) 23 { 24 int v=edge[e].to; 25 if(v==fa) continue; 26 dfs(v,u); 27 if(edge[e].val==1) SG[u]^=(SG[v]+1); 28 else SG[u]^=(SG[v]^(edge[e].val%2)); 29 } 30 } 31 int main() 32 { 33 scanf("%d",&T); 34 for(RI cas=1;cas<=T;++cas) 35 { 36 scanf("%d",&n); 37 clr(head,-1); cnt=0; 38 for(RI i=1;i<n;++i) 39 { 40 scanf("%d%d%d",&x,&y,&z); 41 addedge(x,y,z);addedge(y,x,z); 42 } 43 dfs(0,0); 44 if(SG[0]) printf("Case %d: Emily\n",cas); 45 else printf("Case %d: Jolly\n",cas); 46 } 47 return 0; 48 }
Green Hackenbush on general rooted graphs
The Fusion Principle:任何環內的節點可以融合成一點而不會改變圖的sg值。(下面我們稱它爲融合原則)
融合原則允許我們把任意一個根圖簡化爲一個等效的可以通過冒號原則(即Colon Principle)簡化爲竹竿的樹。
同樣,上面的三個圖,每個圖都相當於nim遊戲的一個堆,三個圖組成了一個nim遊戲。接下來我們要找到這些圖等價的nim堆,方便我們解決問題。這要用到融合原則。我們可以把兩個相鄰的節點合成一個節點,並把它們之間的邊彎曲,變成一個圈。一個圈是把自己作爲邊的另一端的一種邊。比如Figure 6.5的最右邊那個雜戲表演者的頭就是一個圈。在GH遊戲中,一個圈是可以被一個葉子(一條沒有任何樹枝與它相連的邊)所代替的,見Figure 6.6中第三幅圖到第四幅圖的轉化。
The Fusion Principle:任何環內的節點可以融合成一點而不會改變圖的sg值。(下面我們稱它爲融合原則)
融合原則允許我們把任意一個根圖簡化爲一個等效的可以通過冒號原則(即Colon Principle)簡化爲竹竿的樹。
如下圖左部分所示的一個門,在地板上的兩個節點是同樣的節點來的(記住地板相當於一個單獨的節點),所以實際上是一個有一個節點與地板相連的三角形,即第二幅圖。融合原則告訴我們,這相當於一個單獨的節點有三個圈與它相連。所以造就了第三幅到第四幅的轉變,過程是把任意兩點收縮成一個圈,3個點兩兩收縮便可得到三個圈。每個圈又相當於長度爲1的竹竿,它們的異或和還是長度爲1的竹竿。
我們會發現,擁有奇數條邊的環可簡化爲一條邊,偶數條邊的環可簡化爲一個節點。例如,在Figure 6.5中的第二幅圖聖誕樹中的有四條邊的環,會縮減成一個節點,所以這聖誕樹最後會簡化爲一個長度爲1的竹竿。相似的,房子上的煙囪變成一個單獨的節點,右邊的窗戶變成一個點,繼續下去,就可以看出房子的SG值爲3。
SG函數的一些題目
LightOJ1199
題意:有n堆石子(1<=n<=100),每一堆分別有xi個石子(1<=xi<=10000), 一次操作可以使一堆石子變成兩堆數目不相等的石子, 最後不能操作的算輸,問先手勝還是後手勝。
對於每一個數下一個拆分爲 (1,n-1),(2,n-2) ... 個.SG函數推到即可;
1 #include<bits/stdc++.h> 2 using namespace std; 3 typedef long long ll; 4 const int maxn=10010; 5 int T,n,SG[maxn],x,ans,vis[maxn]; 6 void getSG(int n) 7 { 8 for(int i=1;i<=n;++i) 9 { 10 memset(vis,0,sizeof vis); 11 for(int j=1;j*2<i;++j) if(i!=j*2) vis[SG[j]^SG[i-j]]=1; 12 for(int j=0;j<maxn;++j){ if(!vis[j]) { SG[i]=j;break; } } 13 } 14 } 15 int main() 16 { 17 scanf("%d",&T); 18 getSG(maxn); 19 for(int cas=1;cas<=T;++cas) 20 { 21 scanf("%d",&n);ans=0; 22 for(int i=1;i<=n;++i) 23 { 24 scanf("%d",&x); 25 ans^=SG[x]; 26 } 27 if(ans) printf("Case %d: Alice\n",cas); 28 else printf("Case %d: Bob\n",cas); 29 } 30 return 0; 31 }
LightOJ1229
題目意思就是給你一行字符串由 '.'和'X'組成,然後兩個人交替將一個‘.’
變成'X'.如果某個人先形成連續的3個‘X’,則這個人就取得勝利。問先手必勝的位置是否存在,
如果存在,有多少個,並依次輸出其位置;這題肯定是枚舉每一個位置,判斷是否可以勝利,
對於SG[x]表示長度爲x的‘.’區間的SG值,然後對於每一個位置(不是'X'的位置),判斷其由
‘.’變成'X'之後是否可以形成連續3個'X'的必勝狀態,是否會形成.XX或XX.或X.X的必敗狀態;如果都不是再去枚舉每一個區間的SG值,再將其異或ans,就得到這一個位置的SG值,爲0必勝,不爲零必敗;
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 #include<vector> 5 using namespace std; 6 typedef long long ll; 7 #define clr(a,val) memset(a,val,sizeof(a)) 8 const int maxn=210; 9 int T,SG[maxn],len; 10 vector<int> v; 11 char s[maxn],s1[maxn]; 12 int getSG(int m) 13 { 14 if(m<0) return 0; 15 if(SG[m]!=-1) return SG[m]; 16 bool vis[maxn];clr(vis,0); 17 for(int i=1;i<=m;++i) vis[getSG(i-3)^getSG(m-i-2)]=1; 18 int t=0; 19 while(vis[t]) ++t; 20 return SG[m]=t; 21 } 22 23 bool check(int x) 24 { 25 strcpy(s1,s); 26 if(s1[x]=='X') return 0; 27 s1[x]='X'; 28 for(int i=0;i<len-2;++i) {if(s1[i]=='X'&&s1[i+1]=='X'&&s1[i+2]=='X') return 1;} 29 for(int i=0;i<len-1;++i) {if(s1[i]=='X'&&s1[i+1]=='X') return 0;} 30 for(int i=0;i<len-2;++i) {if(s1[i]=='X'&&s1[i+2]=='X') return 0;} 31 int j=-1,f=0,ans=0; 32 for(int i=0;i<len;++i) 33 { 34 if(s1[i]=='X') 35 { 36 if(f) ans^=getSG(i-j-5); 37 else ans^=getSG(i-j-3),f=1; 38 j=i; 39 } 40 } 41 ans^=getSG(len-j-3); 42 return ans==0; 43 } 44 45 int main() 46 { 47 scanf("%d",&T); 48 memset(SG,-1,sizeof SG); 49 for(int cas=1;cas<=T;++cas) 50 { 51 scanf("%s",s); 52 v.clear(); 53 len=strlen(s); 54 for(int i=0;i<len;++i) 55 { 56 if(check(i)) v.push_back(i+1); 57 } 58 printf("Case %d:",cas); 59 if(v.size()) 60 { 61 for(int i=0;i<v.size();++i) printf(" %d",v[i]); 62 puts(""); 63 } 64 else printf(" 0\n"); 65 } 66 67 return 0; 68 }
LightOJ1344
現在有幾串手鐲,每個手鐲上都有一些珠子,每個珠子都有權值,Aladdin 和 Genie 輪流取珠子,當取了一顆珠子後,這個手鐲上所有權值大於等於這顆珠子權值的珠子,都要被刪去。因此,這個手鐲就會變成新的幾個手鐲(因爲被切割了)。
如,5-1-7-2-4-5-3 這串手鐲,選第一顆珠子,權值爲5,因此,5,7,5 就要被刪去。手鐲變成了新的 1,2-4,3 三個手鐲。 最後誰不能拿,誰輸;
題解:每個手鐲都是獨立的,因此可以異或每個手鐲的 sg 值求解。而每個手鐲,可以在某些結點被取走珠子,變成新的幾段,任意一種情況的 sg 值,便是新分成的幾段的 sg 值異或起來。再將每一種情況的 sg 值,記錄在 vis 中,查找沒出現過的最小的值,便是這個手鐲的 sg 值。
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int maxn = 55; 4 int sg[maxn][maxn]; 5 int arr[maxn][maxn], num[maxn]; 6 int sgtmp[maxn]; 7 int ret[maxn][maxn]; 8 9 struct node { 10 int x, y; 11 } output[maxn * maxn]; 12 bool operator==(node a,node b){return a.x==b.x&&arr[a.x][a.y]==arr[b.x][b.y];} 13 bool cmp(node a,node b) 14 { 15 if(a.x==b.x) return arr[a.x][a.y]<arr[b.x][b.y]; 16 return a.x<b.x; 17 } 18 19 int dfs(int now,int l,int r) 20 { 21 if(l>r) return 0; 22 if(l==r) return sg[l][r]=1; 23 if(sg[l][r]!=-1) return sg[l][r]; 24 25 int vis[maxn]; 26 memset(vis,0,sizeof(vis)); 27 28 for(int i=l;i<=r;++i) 29 { 30 int tmp=0,last=-1; 31 32 for(int j=l;j<=r;++j) 33 { 34 if(arr[now][j]<arr[now][i]) 35 { 36 last = j; 37 break; 38 } 39 } 40 for(int j = last + 1; j <= r && last != -1; ++j) 41 { 42 if(arr[now][j] >= arr[now][i]) 43 { 44 tmp ^= dfs(now, last, j - 1); 45 last = -1; 46 for (int k = j + 1; k <= r; ++k) 47 { 48 if (arr[now][k] < arr[now][i]) 49 { 50 last = j = k; 51 break; 52 } 53 } 54 } 55 } 56 if(last != -1) tmp ^= dfs(now, last, r); 57 vis[tmp] = 1; 58 if (l == 1 && r == num[now]) ret[now][i] = tmp; 59 } 60 for(int i = 0;;++i) 61 { 62 if(vis[i]==0) 63 { 64 sg[l][r]=i; 65 return i; 66 } 67 } 68 } 69 int main() 70 { 71 int t, k, ca = 1; 72 scanf("%d", &t); 73 while (t--) 74 { 75 int ans = 0; 76 scanf("%d", &k); 77 for(int i = 1; i <= k; ++i) 78 { 79 memset(sg, -1, sizeof(sg)); 80 scanf("%d", &num[i]); 81 for(int j = 1; j <= num[i]; ++j) scanf("%d", &arr[i][j]); 82 sgtmp[i] = dfs(i, 1, num[i]); 83 ans ^= sgtmp[i]; 84 } 85 if (ans == 0) printf("Case %d: Genie\n", ca++); 86 else 87 { 88 int cnt = 0; 89 printf("Case %d: Aladdin\n", ca++); 90 memset(output, 0, sizeof(output)); 91 for (int i = 1; i <= k; ++i) 92 for (int j=1;j<=num[i];++j){if((ans^sgtmp[i]^ret[i][j])==0) output[cnt++] = {i, j};} 93 sort(output,output+cnt,cmp); 94 cnt=(int)(unique(output,output+cnt)-output); 95 for(int i = 0; i < cnt; ++i) 96 printf("(%d %d)", output[i].x, arr[output[i].x][output[i].y]); 97 printf("\n"); 98 } 99 } 100 return 0; 101 }