組合賽局
玩遊戲
組合賽局的定義
- 資訊公開
- 兩名玩家
- 無運氣成分
資訊公開
所有玩家皆可看到整個遊戲的局勢
這排除了德州撲克、橋牌等所有玩家皆可看到整個遊戲的局勢
兩名玩家
僅有兩名玩家輪流進行遊戲無運氣成分
操作時不帶任何機率成分
這排除了擲骰子,以及(又是)德州撲克組合賽局的分類
雙方操作
- 無偏賽局(雙方可進行的操作相同)
- 有偏賽局(雙方可進行的操作不同)
如象棋就是有偏賽局,因為雙方的棋子並非共用的
勝利條件
- 標準賽局(最後操作者獲勝)
- 匱乏賽局(最後操作者落敗)
重複可能
- 無環賽局:不會重複觸發同個局面
- 有環賽局:會重複觸發同個局面
像是象棋或是西洋棋,能夠來回走重複觸發同個局面
尼姆遊戲(Nim Game)
撿石頭
會尼姆了沒有
媽媽媽媽,你看,有石頭
😭😭😭😭😭
會尼姆了沒有
有一堆石頭,玩家可從一堆中取任意數量,取到最後一顆者獲勝
我全都要就好了
兩堆呢?
輪到先手/後手時,拿取石頭維持兩堆石頭相等,直到取到最後一顆石頭
如果初始狀態為兩堆相等,則後手必勝,反之先手必勝
終止狀態
遊戲結束時的狀態稱為終止狀態
在上述的遊戲中,終止狀態即為兩堆石頭為0
並且終止狀態時兩堆石頭相等
先手/後手每次操作都能夠控制此狀態(使兩堆石頭相等)
持續控制此狀態直到抵達終止狀態即獲勝
多堆石頭
有 堆石頭,每堆數量可以不同
玩家可以從一堆中拿出至少一顆石頭(只能拿一堆中的石頭)
Normal:拿到最後一顆者勝
Misère:拿到最後一顆者敗
尼姆數
每一堆的數量做 計算,得到尼姆數,如:

尼姆數為 ,記做
Normal,Misère必勝條件
如果初始狀態時尼姆數不為0,則先手有必勝法,反之後手

Proof
遊戲終止狀態
每堆石頭為0,玩家無法拿取,落敗
這是一個P-position(前面一個移動的玩家獲勝)
每堆石頭為0,尼姆數也是0
P-position為nim(0)
必定有一個合法的操作使尼姆數變成0
令S為尼姆數的位數
尼姆數的第S位必定為1
且石頭堆裡必定至少有一堆石頭數的S位為1(設為 )
⊕ ⊕...⊕ =nim
⊕ ⊕...⊕ ⊕nim=nim⊕nim=
且 ⊕nim<
故必定能將第i堆的數量減少至 ⊕nim,使得新的nim為
無任何一個合法的操作使
尼姆數從0變成0
⊕ ⊕...⊕ =0
假設我要更改 至
必不等於
⊕ 必不等於0
則 ⊕ ⊕...⊕ ⊕( ⊕ ) =0⊕( ⊕ )≠0
無任何一個合法的操作使尼姆數從0變成0
為何不用其他方法檢查狀態
不符合上述兩個特性
先手無法控制or後手可以控制
例題
Misère要怎麼操作
與normal一樣,直到場上只剩下一堆石頭>1的堆
取該堆石頭至0 or 1使得場上有奇數個堆
獲勝
Sprague-Grundy 定理
將Nim number推廣至所有無偏無環賽局上
Grundy數
如果一遊戲之初始狀態 Grundy 數是 0,則先手必敗局;反之必勝
聽起來很Nim
怎麼計算?
遞迴
G(原狀態)=mex({下一步狀態,下一步狀態,......})
mex({})為在此集合中,最小的,非負整數的,不在此集合中的數
會有一個終止狀態當作遞回終點
Grundy num=
是指第i堆尼姆堆的狀態
玩家變成一次只能取1,2,3顆石頭
怎麼用Grundy來解?
考慮各種狀況
0顆石頭,G(0)=0(終止狀態)
1顆石頭,G(1)=mex({G(0)})=1
2顆石頭,G(2)=mex({G(0),G(1)})=2
3顆石頭,G(3)=mex({G(0,G(1),G(2)})=3
4顆石頭,G(4)=mex({G(1),G(2),G(3)})=0
5顆石頭,G(5)=mex({G(2),G(3),G(4)})=1
6顆石頭,G(6)=mex({G(3),G(4),G(5)})=2
7顆石頭,G(7)=mex({G(4),G(5),G(6)})=3
你發現了甚麼
G(n)=n%4
你會了
好欸
甚至不用遞迴
AC code
#include <bits/stdc++.h>
using namespace std;
int main () {
int t;
cin>>t;
while (t--) {
int n;
cin>>n;
int ans=0,x;
for(int i=0;i<n;i++){
cin>>x;
ans=ans^(x%4);
}
if(ans==0)cout<<"second\n";
else cout<<"first\n";
}
return 0;
}需遞迴的實作題
CSES2207 TLE+stack overflow code
#include <bits/stdc++.h>
using namespace std;
vector<int>dp(1e6+5,-1);
int G(int n){
if(n==1||n==2)return 0;
if(dp[n]!=-1)return dp[n];
else{
unordered_set<int>s;
for(int i=1;i<n-i;i++){
s.insert(G(i)^G(n-i));
}
int ans=0;
while(s.count(ans))ans++;
dp[n]=ans;
return ans;
}
}
int main () {
dp[1]=0;
dp[2]=0;
int t;
cin>>t;
while (t--) {
int n;
cin>>n;
if(!G(n))cout<<"second\n";
else cout<<"first\n";
}
return 0;
}
怎辦?
找規律?沒啥規律
找後手必勝的上限
1~50000中,後手必勝時的n
1,2,4,7,......,929,932,1022
???1022之後怎麼沒東西了
猜測(通靈)1022以上先手必勝
所以判<=1022的case即可
CSES2207 AC code
#include <bits/stdc++.h>
using namespace std;
vector<int>dp(1000005,-1);
int G(int n){
if(n==1||n==2)return 0;
if(dp[n]!=-1)return dp[n];
else{
unordered_set<int>s;
for(int i=1;i<n-i;i++){
s.insert(G(i)^G(n-i));
}
int ans=0;
while(s.count(ans))ans++;
dp[n]=ans;
return ans;
}
}
int main () {
dp[1]=0;
dp[2]=0;
int t;
cin>>t;
while (t--) {
int n;
cin>>n;
if(n<=1222){
if(!G(n))cout<<"second\n";
else cout<<"first\n";
}
else cout<<"first\n";
}
}當然也可以用迴圈寫
甚至可以打表,後手必勝只有41種可能
謝謝大家
你會撿石頭
組合賽局
By 硼/Boron
組合賽局
- 114