找出游戲的必勝的策略(博弈論的學習)


題目:硬幣游戲1,Alice和Bob在玩這樣一個游戲。給定k個數字a1,a2,···ak。 一開始,有x枚硬幣,Alice和Bob輪流取硬幣。每次所取硬幣的枚數

一定要在a1,a2···,ak當中。Alice先取,取走最后一枚硬幣的一方獲勝。當雙方都采取最有策略時,誰會獲勝?假定a1a2···ak中一定有1

限制條件:1<=x<=10000 1<=k<=100 1<=ai<=k

樣例:

輸入

x=9

k=2

a={1,4}

輸出

Alice

樣例2

x=10

k=2

a={1,4}

輸出

Bob

下面考慮輪到自己的時,還有j枚硬幣的情況

1、題目規定取光所有硬幣就獲勝,這等價於輪到自己時如果沒有了硬幣就失敗了。因此,j=0時是必敗態

2、如果對於某個i(1<=i<=k),j-ai是必敗態的話,j就是必勝態。(如果當前有j枚硬幣,只要取走ai枚,對手就必敗->自己必勝)

3、如果對於任意的i(1<=i<=k),j-ai都是必勝態的話,j就是必敗態(不論怎么取,對手都必勝->自己必敗)

根據上面這些規則,我們利用動態規划算法按照j從小到大的順序計算必勝態必敗態。只要看x是必勝態還是必敗態,我們就知道誰會獲勝了

像這樣,通過考慮各個狀態的勝負條件,判斷必勝態和必敗態,是有勝敗的游戲的基礎

看代碼

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int main()
{
    bool win[10005];
    int x,k;
    int a[110];
    cin>>x>>k;
    for(int i=0;i<k;i++)
        cin>>a[i];
    win[0]=false;//0枚硬幣必敗
    for(int i=1;i<=x;i++)
    {
        win[i]=false;//先初始化為為必敗態
        for(int j=0;j<k;j++)
        {
            win[i]|=a[j]<=i&&!win[i-a[j]];//異或運算,有一個為必敗態則為必勝態
        }
    }
    if(win[x])
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
    return 0;
}

 

2、A Funny Game

題目:n枚硬幣排成一個圈。Alice和Bob輪流從中取一枚或兩枚硬幣。不過,取兩枚時,所取的兩枚硬幣必須是連續的。硬幣取走之后留下空位

,相隔空位的硬幣視為不連續。Alice開始先取,取走最后一枚硬幣的一方獲勝。當雙方都采取最優策略時,誰會獲勝

0<=n<=1000000

輸入

n=1

輸出

Alice

輸入

n=3

輸出

Bob

n高達1000000,考慮到還有將連續部分分裂成幾段等的情況,狀態數非常的多,搜索和動態規划都難以勝任。需要更加巧妙地判斷勝敗關系

首先,試想一下如下情況。能夠把所有硬幣分解成兩個完全相同的狀態,是必敗態還是必勝態呢?

事實上,是必敗態。不論自己采取何種策略,對手是要在另一組采取相同的策略,就又回到了分成兩個相同的組的狀態了

不斷循環下去,總會輪到自己沒有硬幣了。也就是說,因為對手取走了最后一枚硬幣而敗北

接下來,回到正題。Alice在第一步取走了一枚或者兩枚硬幣之后,原本成圈的硬幣變成了長度為n-1或者n-2的鏈。這樣只要Bob在中間位子

根據鏈長的奇偶性,取走一枚或者兩枚硬幣,就可以把所有硬幣正好分為兩個長度相同的鏈

這也正如我們前面說的必敗態。也就是說Alice必敗,Bob必勝,只不過當n<=2時,Alice可以在第一次取光,所以勝利的是Alice。在這類游戲中

做出對稱的狀態再完全模仿對手的策略常常是有效的

看代碼

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int main()
{
    int n;
    cin>>n;
    if(n<=2)
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
    return 0;
}

 

3、Euclid's Game

讓我們看一下這個以輾轉相除法為基礎的游戲

給定兩個整數a,b。Stan和Ollie輪流從較大的數字中減去較小數字的倍數。這里的倍數是指1倍,2倍這樣的正整數倍,並且相減后的結果不能

小於零。Stan先手,在自己的回合將其中一個數變為0的一方獲勝。當雙方都采取最優策略時,誰會獲勝?

輸入

a=64,b=12

輸出

Stan wins

輸入

a=15,b=24

輸出

Ollie wins

讓我們來找找看該問題中必勝態和必敗態。首先,如果a>b則交換,假設a<b。另外,如果b已經是a的倍數了則必勝,所以假設b並非a的倍數

此時,a和b的關系按照自由度的觀點。可以分為以下兩類

b-a<a的情況

b-a>a的情況

對於第一種情況,只能從b中減去a,沒有選擇的余地。相對的,對於第二種情況,有從b中減去a,減去2a,或者更高的倍數的情況

對於第一種情況,要判斷必勝還是必敗並不難。因為沒有選擇的余地,如果b-a之后所得狀態是必敗態的話,他就是必勝態,如果得到的是必勝態的話,它就是必敗態

例如,從(4,7)這個狀態出發就完全沒有選擇的余地,按照

(4,7)->(4,3)->(1,3)的順序,輪到(1,3)的一方將獲勝

所以有必勝->必敗->必勝 可見(4,7)是必勝態

接下來,我們來看第二種情況是必勝態還是必敗態。假設x是使得b-ax<a的整數,考慮一下,從b中減去a(x-1)的情況。例如對於(4,19)則減去12

此時,接下來的狀態成了前邊講過的沒有選擇的情況,如果改狀態是必敗的話,則當前狀態就是必勝態。

那么,如果減去a(x-1)后的狀態是必勝態的話,該如何是好?  此時,從b中減去ax后的狀態就是減去a(x-1)后的狀態唯一可以轉移到的狀態,根據假設,減去a(x-1)是必勝態,所以該狀態是必敗態。因此是必勝態

由此可知,對於第二種情況,總是必勝的。所以,從初始狀態開始,最先到達有自由度的第二種狀態的一方必勝

看代碼

#include<iostream>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
#define INF 0x3f3f3f
int a,b;
void solve(int a,int b)
{
    bool f=true;
    while(1)
    {
        if(a>b)
            swap(a,b);
        if(b%a==0)
            break;
        if(b-a>a)
            break;
        b-=a;
        f=!f;
    }
    if(f)
        cout<<"Stan wins"<<endl;
    else
        cout<<"Ollie wins"<<endl;
}
int main()
{
    cin>>a>>b;
    solve(a,b);
    return 0;
}

 4、Nim游戲

算法樹上有點簡略,沒有看懂,參考資料:https://baike.baidu.com/item/Nim游戲/6737105?fr=aladdin

題目大意:有n堆石子,每堆石子有a[i]個。Alice和Bob輪流從非空的石子中取走至少一顆石子。Alice先取,取光所有石子的一方獲勝。當雙方都采取最優策略時

,誰會獲勝?

限制條件:1<=n<=1000000   1<=ai<=10^9

樣例:

輸入

n=3

a={1,2,4}

輸出:

Alice

讓我們來看看這個游戲,該游戲的策略也成為了許多游戲的基礎。要判斷該游戲的勝負只要用異或運算就好了。有以下結論:

a1^a2^...^an!=0    必勝態

a1^a2^...^an==0  必敗態

因此,只要計算異或值就知道誰勝了

分析一下為什么是這樣的:

有三種情況:

1、無法進行移動     那么它就是必敗態

2、可以移動到必敗態        那么它就是必勝態

3、所有的移動都導致必勝態    那么它就是必敗態

第一個命題顯然,無法進行移動只有一個,就是全0,異或仍然是0。

第二個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an<>0,一定存在某個合法的移動,將ai改變成ai'后滿足a1^a2^...^ai'^...^an=0。不妨設a1^a2^...^an=k,則一定存在某個ai,它的二進制表示在k的最高位上是1(否則k的最高位那個1是怎么得到的)。這時ai^k<ai一定成立。則我們可以將ai改變成ai'=ai^k,此時a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。

第三個命題,對於某個局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某個合法的移動,將ai改變成ai'后滿足a1^a2^...^ai'^...^an=0。因為異或運算滿足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以將ai改變成ai'不是一個合法的移動。證畢。

看代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const ll maxa=32050;
#define INF 0x3f3f3f3f3f3f
ll n;
ll a[maxn];
void solve()
{
    ll x=0;
    for(int i=0;i<n;i++)
        x^=a[i];
    if(x!=0)
        cout<<"Alice"<<endl;//總存在一個變化使得x==0,使得它為必敗態,所以它本身是必勝態
    else
        cout<<"Bob"<<endl;
}
int main()
{
    cin>>n;
    for(int i=0;i<n;i++)
        cin>>a[i];
    solve();
    return 0;
}

 

題目大意:排成直線的格子上放有n個棋子。棋子i放在左數第a[i]個格子上。Georgia和Bob輪流選擇一個棋子向左移動。每次可以移動一個或者任意多格】

但是不允許反超其他的棋子,也不允許將兩個棋子放在同一個格子內。無法進行移動的失敗。假設Georgia先移動,當雙方都采取最優策略時,誰會獲勝?

限制條件:1<=n<=1000 1<=a[i]<=10000

輸入

3

1 2 3

輸出

Bob

輸入

8

1 5 6 7 9 12 14 17

輸出

Georgia

思路:如果將棋子兩兩成對當做整體進行考慮,我們就可以把這個游戲轉化為Nim游戲了。先按棋子個數分奇偶情況討論。

我們可以將每對棋子看作Nim中的一堆石子。石子堆中石子的個數等於兩個棋子的間隔、

讓我們看一下為什么可以這樣轉化。考慮其中的某一對石子,將右邊的棋子向左移動就相當於從Nim的石子堆中取走石子,另一方面,將左邊的石子向左移動,就相當於增加石子。這就與Nim游戲不同了。但是,即便對手增加了石子數量,只要將所加部分減回去就回到原來的狀態了。所以這個游戲的勝負和Nim游戲勝負是一樣的

看代碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=2e5+10;
const ll maxa=32050;
#define INF 0x3f3f3f3f3f3f
int main()
{
    int n,x=0;
    int a[1050];
    cin>>n;
    for(int i=0;i<n;i++)
    {
        cin>>a[i];
    }
    if(n%2==1)
        a[n++]=0;
    sort(a,a+n);
    for(int i=1;i<n;i++)
    {
        x^=(a[i]-a[i-1]-1);
    }
    if(x==0)
        cout<<"Bob"<<endl;
    else
        cout<<"Georgia"<<endl;
    return 0;
}

 5、硬幣游戲2

Alice和Bob在玩這樣一個游戲。給定k個數字q1,a1···ak。一開始,有n堆硬幣,每堆各有xi枚硬幣。Alice和Bob輪流選出其中一堆硬幣,從中取出硬幣。每次所取i硬幣的枚數一定要在a1,a2

···ak當中。Alice先取,取光硬幣的一方獲勝。當雙方都采取最優策略時誰會獲勝?保證a1,a2···ak一定有個1

限制條件:1<=n<=1000000   1<=k<=100 1<=xi,ai<=10000

輸入

n=3

k=3

a={1,3,4}

x={5,6,7}

輸出

Alice

這和我們之前介紹的硬幣問題1相似,只不過那道題中只有一堆硬幣,而本題有n堆。如果依然用動態規划的話,狀態數高達O(x1x2···xn)

在此,為了高效的求解該問題,引出Grundy值這一重要概念。利用它,不光這個游戲,其他許多游戲都可以轉化成前面所介紹的Nim

讓我們再來考慮一下只有一堆硬幣的情況,qy硬幣枚數x所對應的Grundy值的計算方法如下。

int grundy(x)

{

  集合S={}

 for(j=1...k)

 if(a[j]<=x])  將grundy(x-a[j])加入到S中

 return 最小的不屬於S的非負整數

}

也就是說這樣的Grundy值就是除了自己走任意一步所能到達的狀態的Grundy值以外的最小非負整數。這樣的Grundy值,和Nim游戲中的一個石子堆類似,有如下性質

Nim中有x顆石子的石子堆,能夠轉移成有0,1,···x-1顆石子的石子堆

從Grundy值為x的狀態出發,能夠轉移到Grundy值為0,1,x-1的狀態

只不過,與Nim不同的是,轉移后的Grundy值也有可能增加。不過,對手總能夠選取合適的策略回到相同的Grundy值的狀態。,所以對勝負沒有影響。

了解了一堆硬幣Grundy值的計算方法之后,就可以將它看作Nim中的一個石子堆。

下面用的動態規划的方法,復雜度為O(xk)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const int maxk=100+10;
const int maxx=1e4+10;
const ll maxa=43200;
#define INF 0x3f3f3f3f3f3f
int n,k,a[maxn],b[maxk];
int grundy[maxx];
void solve()
{
    //輪到自己時剩下0枚必敗
    grundy[0]=0;
    int max_a=*max_element(a,a+n);//求取最大元素
    for(int i=1;i<=max_a;i++)
    {
        set<int> s;
        for(int j=0;j<k;j++)
        {
            if(b[j]<=i)
                s.insert(grundy[i-b[j]]);
        }
        int g=0;
        while(s.count(g)!=0)//count用來判斷g出現的次數,在這里只有0和1之分
        g++;
        grundy[i]=g;
    }
    //判斷勝負
    int x=0;
    for(int i=0;i<n;i++)
    x^=grundy[a[i]];
    if(x!=0)
        cout<<"Alice"<<endl;
    else
        cout<<"Bob"<<endl;
}
int main()
{
    cin>>n>>k;
    for(int i=0;i<n;i++)
        cin>>a[i];
    for(int i=0;i<k;i++)
        cin>>b[i];
    solve();
    return 0;
}

  

6、兩個人在玩如下游戲

准備一張分成w*h的格子的長方形紙張,兩人輪流切割紙張。要沿着格子的邊界切割,水平或者垂直的將紙張切成兩部分。切割了n次之后就得到了n+1張紙,每次選擇切得的某一張

再進行切割。首先切出只有一個格子的紙張的一方獲勝。當雙方都采取最優策略時,先手必勝還是必敗

限制條件

2<=w,h<=200

這道題也能用Grundy值來計算。當w*h的紙張分成兩張時,假設所分得的紙張的Grundy值分別為g1,g2,則這兩張紙對應的狀態的Grundy值可以表示為g1^g2

看代碼

#include<iostream>
#include<cstdio>
#include<cstring>
#include<stdio.h>
#include<string.h>
#include<cmath>
#include<math.h>
#include<algorithm>
#include<set>
#include<queue>
#include<map>
typedef long long ll;
using namespace std;
const ll mod=1e9+7;
const int maxn=1e6+10;
const int maxk=100+10;
const int maxx=1e4+10;
const ll maxa=43200;
#define INF 0x3f3f3f3f3f3f
int a[210][210];
int mem[210][210];
int grundy(int w,int h)
{
    if(mem[w][h]!=-1)
        return mem[w][h];
    set<int> s;
    for(int i=2;w-i>=2;i++)
        s.insert(grundy(i,h)^grundy(w-i,h));
    for(int i=2;h-i>=2;i++)
        s.insert(grundy(w,i)^grundy(w,h-i));
    int g=0;
    while(s.count(g)!=0)
        g++;
    return mem[w][h]=g;
}
void solve(int w,int h)
{
    if(grundy(w,h)!=0)
        cout<<"WIN"<<endl;
    else
        cout<<"LOSE"<<endl;
}
int main()
{
    int w,h;
    memset(mem,-1,sizeof(mem));
    cin>>w>>h;
    solve(w,h);
    return 0;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM