博弈論 -Nim遊戲(臺階 + SG函數 + 集合 + 拆分)

博弈論 -Nim遊戲(臺階 + 集合 + 拆分)

1、Nim遊戲

給定n堆石子,兩位玩家輪流操作,每次操作可以從任意一堆石子中拿走任意數量的石子(可以拿完,但不能不拿),最後無法進行操作的人視爲失敗。

問如果兩人都採用最優策略,先手是否必勝。

輸入格式
第一行包含整數n。

第二行包含n個數字,其中第 i 個數字表示第 i 堆石子的數量。

輸出格式
如果先手方必勝,則輸出“Yes”。

否則,輸出“No”。

數據範圍
1≤n≤105,
1≤每堆石子數≤109

輸入樣例:
2
2 3
輸出樣例:
Yes

分析:

n:a1,a2,...,an假設n堆石子的石子數量分別爲:a_1,a_2,...,a_n。

000...0=0①、若每一堆石子都爲0,則每一堆石子數量異或值0\bigoplus0\bigoplus...\bigoplus0=0,此時先手必敗。

a1a2...ai...an0a1a2...ai...an=x.x1ka1anaiaik1②、若a_1\bigoplus a_2\bigoplus...\bigoplus a_i\bigoplus ...\bigoplus a_n≠0,設a_1\bigoplus a_2\bigoplus...\bigoplus a_i\bigoplus ...\bigoplus a_n=x.\\\qquad假設x的二進制表示中最高位的1在第k位,則a_1到a_n中必存在一個數a_i,a_i的第k位是1。
aix<aii使aix\qquad那麼有a_i\bigoplus x<a_i,現從第i堆石子拿出一些石子,使得剩下a_i\bigoplus x個石子。
a1a2...(aix)...an=a1a2...ai...anx=xx=0\qquad於是剩下的所有石子的異或值爲:\\\qquad a_1\bigoplus a_2\bigoplus...\bigoplus (a_i\bigoplus x)\bigoplus ...\bigoplus a_n=a_1\bigoplus a_2\bigoplus...\bigoplus a_i\bigoplus ...\bigoplus a_n\bigoplus x=x\bigoplus x=0。

a1a2...ai...an=00③、若a_1\bigoplus a_2\bigoplus...\bigoplus a_i\bigoplus ...\bigoplus a_n=0,則無論如何都不能保持所有堆的石子數量的異或值爲0。
iai使0a1a2...ai...an=0(a1a2...ai...an)(a1a2...ai...an)=aiai=0ai=ai\qquad證明:假設將第i堆石子數量變爲a_i',使得各堆的石子數量的異或值爲0,\\\qquad即a_1\bigoplus a_2\bigoplus...\bigoplus a_i'\bigoplus ...\bigoplus a_n=0。\\\qquad計算(a_1\bigoplus a_2\bigoplus...\bigoplus a_i\bigoplus ...\bigoplus a_n)\bigoplus (a_1\bigoplus a_2\bigoplus...\bigoplus a_i'\bigoplus ...\bigoplus a_n)=a_i\bigoplus a_i'=0。\\\qquad這說明a_i=a_i',即沒有拿石子,矛盾!

0使0於是,只要先手的情況下,各堆石子數量的異或值不爲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假設n堆石子的石子數量分別爲:a_1,a_2,...,a_n。

000...0=0①、若每一堆石子都爲0,則每一堆石子數量異或值0\bigoplus0\bigoplus...\bigoplus0=0,此時先手必敗。

0AxBBA②、若所有奇數級臺階上的石子數量都爲0,僅剩下偶數級臺階上存在石子,先手必敗。\\\qquad因爲若A從偶數級臺階上拿x個石子到奇數級臺階上,B就從該奇數級臺階上將這些石子拿到下一個偶數級臺階,\\\qquad那麼B必然在A先到達地面。

0\qquad這樣,我們就可以通過第一題的方法,控制奇數級臺階上的石子的異或值爲0。

xx0若對手從偶數級臺階拿x,我們就將這x個石子拿到下一個偶數級臺階,保持奇數級臺階的石子數量異或值爲0。

x0若對手從奇數級臺階拿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函數

SGSG函數: SGgg(x)=mex{g(y)yx}對於一個給定的有向無環圖,定義關於圖的每個頂點的SG函數g如下:g(x)=mex\{ g(y) | y是x的後繼 \}。mex其中,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

分析:

SGx我們把每一堆石子的初始數量看作一個狀態的起始位置,以該點爲起點求SG函數,設起點爲x。

SG(x)0xxSG(x)=0①、若SG(x)≠0,從x必能走到x',且SG(x')=0。

SG(x)=0xSG(x)0②、若SG(x)=0,要麼必敗,要麼下個狀態x',SG(x')≠0。

示例:

1025若某堆石子初始數量爲10,每次能夠拿2個或5個,則狀態轉移圖如下:
在這裏插入圖片描述
0SG1()首先終止狀態標記爲0,表示失敗。\\接着倒推與終止狀態直接相連的節點,顯然這些節點對應的SG函數爲1(不屬於後繼節點的最小自然數)。

依次反推即可,這個過程由遞歸來實現

nn另外,本題有n堆石子,就有n個狀態轉移圖。

a1a2...an0這與第一題相似,考慮a_1\bigoplus a_2\bigoplus ...\bigoplus a_n是否爲0即可。

a1SG(a1)SG(ai)i只不過這裏的a_1應當取SG(a_1),因爲SG(a_i)代表着第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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章