博弈論 -Nim遊戲(臺階 + 集合 + 拆分)
1、Nim遊戲
給定n堆石子,兩位玩家輪流操作,每次操作可以從任意一堆石子中拿走任意數量的石子(可以拿完,但不能不拿),最後無法進行操作的人視爲失敗。
問如果兩人都採用最優策略,先手是否必勝。
輸入格式
第一行包含整數n。
第二行包含n個數字,其中第 i 個數字表示第 i 堆石子的數量。
輸出格式
如果先手方必勝,則輸出“Yes”。
否則,輸出“No”。
數據範圍
1≤n≤105,
1≤每堆石子數≤109
輸入樣例:
2
2 3
輸出樣例:
Yes
分析:
假設n堆石子的石子數量分別爲:a1,a2,...,an。
①、若每一堆石子都爲0,則每一堆石子數量異或值0⨁0⨁...⨁0=0,此時先手必敗。
②、若a1⨁a2⨁...⨁ai⨁...⨁an=0,設a1⨁a2⨁...⨁ai⨁...⨁an=x.假設x的二進制表示中最高位的1在第k位,則a1到an中必存在一個數ai,ai的第k位是1。
那麼有ai⨁x<ai,現從第i堆石子拿出一些石子,使得剩下ai⨁x個石子。
於是剩下的所有石子的異或值爲:a1⨁a2⨁...⨁(ai⨁x)⨁...⨁an=a1⨁a2⨁...⨁ai⨁...⨁an⨁x=x⨁x=0。
③、若a1⨁a2⨁...⨁ai⨁...⨁an=0,則無論如何都不能保持所有堆的石子數量的異或值爲0。
證明:假設將第i堆石子數量變爲ai′,使得各堆的石子數量的異或值爲0,即a1⨁a2⨁...⨁ai′⨁...⨁an=0。計算(a1⨁a2⨁...⨁ai⨁...⨁an)⨁(a1⨁a2⨁...⨁ai′⨁...⨁an)=ai⨁ai′=0。這說明ai=ai′,即沒有拿石子,矛盾!
於是,只要先手的情況下,各堆石子數量的異或值不爲0,我們可以拿一些石子使得異或值爲0,直到所有石子被拿空。
而輪到對手時,始終是異或值爲零的情況,對手拿完後,均會使得異或值非零。
因此,只需判斷各堆石子的異或值是否爲零即可。
代碼:
#include<iostream>
using namespace std;
int main()
{
int n;
cin>>n;
int res=0;
while(n--)
{
int x;
cin>>x;
res^=x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
2、臺階-Nim遊戲
現在,有一個n級臺階的樓梯,每級臺階上都有若干個石子,其中第i級臺階上有ai個石子(i≥1)。
兩位玩家輪流操作,每次操作可以從任意一級臺階上拿若干個石子放到下一級臺階中(不能不拿)。
已經拿到地面上的石子不能再拿,最後無法進行操作的人視爲失敗。
問如果兩人都採用最優策略,先手是否必勝。
輸入格式
第一行包含整數n。
第二行包含n個整數,其中第i個整數表示第i級臺階上的石子數ai。
輸出格式
如果先手方必勝,則輸出“Yes”。
否則,輸出“No”。
數據範圍
1≤n≤105,
1≤ai≤109
輸入樣例:
3
2 1 3
輸出樣例:
Yes
分析:
假設n堆石子的石子數量分別爲:a1,a2,...,an。
①、若每一堆石子都爲0,則每一堆石子數量異或值0⨁0⨁...⨁0=0,此時先手必敗。
②、若所有奇數級臺階上的石子數量都爲0,僅剩下偶數級臺階上存在石子,先手必敗。因爲若A從偶數級臺階上拿x個石子到奇數級臺階上,B就從該奇數級臺階上將這些石子拿到下一個偶數級臺階,那麼B必然在A先到達地面。
這樣,我們就可以通過第一題的方法,控制奇數級臺階上的石子的異或值爲0。
若對手從偶數級臺階拿x,我們就將這x個石子拿到下一個偶數級臺階,保持奇數級臺階的石子數量異或值爲0。
若對手從奇數級臺階拿x,此時奇數級臺階的石子數量的異或值必定非零,我們就利用第一題的做法,把奇數級臺階的石子數量異或值變成0。
於是,我們只需判斷所有奇數級臺階上的石子數量的異或值是否爲零即可。
代碼:
#include<iostream>
using namespace std;
int main()
{
int n;
cin>>n;
int res=0;
for(int i=1;i<=n;i++)
{
int x;
cin>>x;
if(i&1) res^=x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
3、SG函數
SG函數: 對於一個給定的有向無環圖,定義關於圖的每個頂點的SG函數g如下:g(x)=mex{g(y)∣y是x的後繼}。其中,mex是求不屬於該集合的最小自然數。
4、集合-Nim遊戲
給定n堆石子以及一個由k個不同正整數構成的數字集合S。
現在有兩位玩家輪流操作,每次操作可以從任意一堆石子中拿取石子,每次拿取的石子數量必須包含於集合S,最後無法進行操作的人視爲失敗。
問如果兩人都採用最優策略,先手是否必勝。
輸入格式
第一行包含整數k,表示數字集合S中數字的個數。
第二行包含k個整數,其中第i個整數表示數字集合S中的第i個數si。
第三行包含整數n。
第四行包含n個整數,其中第i個整數表示第i堆石子的數量hi。
輸出格式
如果先手方必勝,則輸出“Yes”。
否則,輸出“No”。
數據範圍
1≤n,k≤100,
1≤si,hi≤10000
輸入樣例:
2
2 5
3
2 4 7
輸出樣例:
Yes
分析:
我們把每一堆石子的初始數量看作一個狀態的起始位置,以該點爲起點求SG函數,設起點爲x。
①、若SG(x)=0,從x必能走到x′,且SG(x′)=0。
②、若SG(x)=0,要麼必敗,要麼下個狀態x′,SG(x′)=0。
示例:
若某堆石子初始數量爲10,每次能夠拿2個或5個,則狀態轉移圖如下:
首先終止狀態標記爲0,表示失敗。接着倒推與終止狀態直接相連的節點,顯然這些節點對應的SG函數爲1(不屬於後繼節點的最小自然數)。
依次反推即可,這個過程由遞歸來實現
另外,本題有n堆石子,就有n個狀態轉移圖。
這與第一題相似,考慮a1⨁a2⨁...⨁an是否爲0即可。
只不過這裏的a1應當取SG(a1),因爲SG(ai)代表着第i堆石子的狀態。
這裏每一堆石子的狀態圖用哈希表來存。
代碼:
#include<iostream>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N = 110 , M = 10010;
int n,k,a[N],f[M];
int sg(int x)
{
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=0;i<k;i++)
if(x-a[i]>=0)
S.insert(sg(x-a[i]));
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
int main()
{
cin>>k;
for(int i=0;i<k;i++) cin>>a[i];
memset(f,-1,sizeof f);
cin>>n;
int res=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
res^=sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
5、拆分-Nim遊戲
給定n堆石子,兩位玩家輪流操作,每次操作可以取走其中的一堆石子,然後放入兩堆規模更小的石子(新堆規模可以爲0,且兩個新堆的石子總數可以大於取走的那堆石子數),最後無法進行操作的人視爲失敗。
問如果兩人都採用最優策略,先手是否必勝。
輸入格式
第一行包含整數n。
第二行包含n個整數,其中第i個整數表示第i堆石子的數量ai。
輸出格式
如果先手方必勝,則輸出“Yes”。
否則,輸出“No”。
數據範圍
1≤n,ai≤100
輸入樣例:
2
2 3
輸出樣例:
Yes
分析:
本題與第四題相似,把每一堆石子看作一個狀態圖,只不過轉移時一堆會分成兩堆,
兩堆的狀態應當是兩堆石子數量的異或值。
代碼:
#include<iostream>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N=110;
int f[N];
int sg(int x)
{
if(f[x]!=-1) return f[x];
unordered_set<int> S;
for(int i=0;i<x;i++)
for(int j=0;j<=i;j++)
S.insert(sg(i)^sg(j));
for(int i=0;;i++)
if(!S.count(i))
return f[x]=i;
}
int main()
{
int n;
cin>>n;
memset(f,-1,sizeof f);
int res=0;
for(int i=0;i<n;i++)
{
int x;
cin>>x;
res^=sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}