博弈論與SG函數

以前寫的關於博弈基礎知識的博客:基礎博弈論【巴什博弈、威佐夫博弈、尼姆博弈、反尼姆博弈】https://blog.csdn.net/ljw_study_in_CSDN/article/details/88356973

先複習一些概念:

  • 先手必勝爲N-position,後手必勝爲P-position。
  • 必敗點(P態):前一個選手(Previous player)將取勝的位置稱爲必敗點。
  • 必勝點(N態) :下一個選手(Next player)將取勝的位置稱爲必勝點。
  • 現在關於P,N的求解有三個規則
    (1):最終態都是P(遊戲規則是:最後不能進行操作的人輸)
    (2):按照遊戲規則,到達當前態的前態都是N的話,則當前態是P
    (3):按照遊戲規則,到達當前態的前態至少有一個P的話,則當前態是N

———————————————————————————————————————————————————————

hdu 1730 Northcott Game

尼姆博弈。

爲什麼是Nim博弈呢?通常的Nim遊戲的定義是這樣的:有若干堆石子,每堆石子的數量都是有限的,合法的移動是“選擇一堆石子並拿走若干顆(不能不拿)”,如果輪到某個人時所有的石子堆都已經被拿空了,則判負(因爲他此刻沒有任何合法的移動)。
先手從某一堆中取x個石子然後後手取y個石子,直到這一堆中沒有石子和黑棋走x步然後白棋走y步直到黑白棋相遇(如果黑白棋子相遇,那先手必輸,因爲先手如果移動,後手緊跟移動,直至先手不能動,故先手必輸)是不是極其相似呢?
假如把每一行都看成一個堆,而每個堆中的石子數爲兩個棋子距離差-1,所以我們只需要對每一堆的“石子數”進行異或計算即可。

#include <bits/stdc++.h>
using namespace std;
int n,m,a,b,x,sum;
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m)
    {
        sum=0;
        for(int i=1;i<=n;i++)
        {
            cin>>a>>b;
            x=abs(a-b)-1;
            sum=sum^x;
        }
        if(sum)printf("I WIN!\n");
        else printf("BAD LUCK!\n");
    }
    return 0;
}

hdu 1850 Being a Good Boy in Spring Festival

求尼姆博弈先手必勝的方法數。

只要選擇的一堆石子數大於其他所有石子堆異或值,則先手必勝,因爲可以把選擇的那堆石子數減少到與其他所有石子堆異或值相等,從而使所有石子堆異或值爲0,使後手開始選擇時面臨的是必敗狀態。

#include <bits/stdc++.h>
using namespace std;
int n,sum,ans,a[110];
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n&&n)
    {
        sum=0;ans=0;
        for(int i=1;i<=n;i++)
        {
            cin>>a[i];
            sum=sum^a[i];
        }
        for(int i=1;i<=n;i++)
            if((sum^a[i])<a[i])ans++;
        printf("%d\n",ans);
    }
    return 0;
}

hdu 1907 John

反尼姆博弈。

尼姆博弈是最先取光石子的人贏,而反尼姆博弈是最先取光石子的人輸。

做法:將所有的石子堆異或,設爲sum,反尼姆博弈中先手必勝有兩種可能:
①sum!=0且存在a[i]>1
②sum=0且所有a[i]=1(因爲每個石子堆只有一個石子且可以證明石子堆個數必爲偶數,所以顯然總是後手最先取光石子,也就是後手必敗,先手必勝)

#include <bits/stdc++.h>
using namespace std;
int t,n,x,sum,cnt;
int main()
{
    ios::sync_with_stdio(false);
    cin>>t;
    while(t--)
    {
        cin>>n;
        sum=0;cnt=0;
        for(int i=1;i<=n;i++)
        {
            cin>>x;
            sum=sum^x;
            if(x==1)cnt++;//統計只有1個石子的石子堆個數
        }
        if((sum!=0&&cnt<n)||(sum==0&&cnt==n))printf("John\n");
        else printf("Brother\n");
    }
    return 0;
}

hdu 2147 kiki’s game

打表NP態,找規律。

從右上角向左下角打表NP態,N態(設爲0)表示先手必勝,P態(設爲1)表示後手必勝,初始位置a[1][m]=1。
遞推到某個位置時,如果它的右邊、上邊、右上邊均爲N態,則它爲P態;如果它的右邊、上邊、右上邊中只要有一個是P態,則它爲N態(先手可以把對手的必勝態P態轉化爲自己的必勝態N態)。

打表代碼(不能AC):

#include <bits/stdc++.h>
using namespace std;
const int N=2010;
int n,m,a[N][N];
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m&&(n||m))
    {
        memset(a,0,sizeof(a));
        a[1][m]=1;
        for(int i=1;i<=n;i++)
            for(int j=m;j>=1;j--)
                if(a[i][j+1]==0&&a[i-1][j]==0&&a[i-1][j+1]==0)a[i][j]=1;
        if(a[n][1]==0)printf("Wonderful!\n");//先手勝
        else printf("What a pity!\n");//後手勝
    }
    return 0;
}

不能AC的原因是超內存(Memory Limit Exceeded),所以要打表前幾項找規律。
顯然可以看出後手勝的概率較低,那麼就找後手勝的規律。

1 1 後手勝

1 2 先手勝
2 1 先手勝
2 2 先手勝

3 1 後手勝
1 3 後手勝
3 2 先手勝
2 3 先手勝
3 3 後手勝

4 1 先手勝
1 4 先手勝
4 2 先手勝
2 4 先手勝
4 3 先手勝
3 4 先手勝
4 4 先手勝

規律:當n和m均爲奇數時,後手勝,否則先手勝。

AC代碼:

#include <bits/stdc++.h>
using namespace std;
int n,m;
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m&&(n||m))
    {
        if((n&1)&&(m&1))printf("What a pity!\n");
        else printf("Wonderful!\n");
    }
    return 0;
}

hdu 1848 Fibonacci again and again

裸的打表SG函數,求sg函數的模板題。

將輸入的三個值對應的sg函數進行異或,異或值爲0則後手勝,否則先手勝。

#include <bits/stdc++.h>
using namespace std;
const int N=1010;
bool vis[N];
int a,b,c,f[N],sg[N];//f[]表示每次能取的數
void get_sg()//打表sg函數
{
    memset(sg,0,sizeof(sg));//sg[0]=0
    for(int i=1;i<=N;i++)//從1開始遍歷,得到sg[i]對應的值
    {
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=15&&f[j]<=i;j++)//遍歷能取的數f[j]
            vis[sg[i-f[j]]]=1;//標記i取了f[j]後對應的sg值
        for(int j=0;j<=N;j++)
            if(vis[j]==0){sg[i]=j;break;}//第一個未標記的非負數即爲sg[i]
    }
}
int main()
{
    ios::sync_with_stdio(false);
    f[1]=1;f[2]=2;
    for(int i=3;i<=15;i++)//f[15]=987<1000,f[16]=1597>1000
        f[i]=f[i-1]+f[i-2];
    get_sg();
    while(cin>>a>>b>>c&&(a||b||c))
    {
        if(sg[a]^sg[b]^sg[c])printf("Fibo\n");
        else printf("Nacci\n");
    }
    return 0;
}

hdu 1536 S-Nim

這題和上題差不多,還是直接打表sg函數。 記得排序一下 f 數組(f[i]表示每次能取的數)。

還有一個比較玄學的細節:vis標記數組寫成了int類型會導致TLE,改成bool類型就AC了。

#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10;
bool vis[N];//這裏的vis一定要定義成bool類型,定義成int類型會超時!
int n,m,k,x,sum,f[N],sg[N];
void get_sg()
{
    memset(sg,0,sizeof(sg));
    for(int i=1;i<=N;i++)
    {
        memset(vis,0,sizeof(vis));
        for(int j=1;j<=n&&f[j]<=i;j++)//默認f[]已經升序排列
            vis[sg[i-f[j]]]=1;
        for(int j=0;j<=N;j++)
            if(vis[j]==0){sg[i]=j;break;}
    }
}
int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n&&n)
    {
        for(int i=1;i<=n;i++)
            cin>>f[i];
        sort(f+1,f+n+1);//一定要保證f[]升序
        get_sg();
        cin>>m;
        while(m--)
        {
            cin>>k;
            sum=0;
            for(int i=1;i<=k;i++)
            {
                cin>>x;
                sum=sum^sg[x];
            }
            if(sum==0)printf("L");
            else printf("W");
        }
        printf("\n");
    }
    return 0;
}

hdu 3980 Paint Chain

題意:n元環,每次取m個連續的石子,最後取不了的判負。
n元環可以經過去一次之後變成n-m元鏈,而鏈可以用Nim和來計算sg值。
可以這樣理解: 對於n-m元鏈,每次選擇一個位置取m個石子,從而把剩餘部分分成了兩堆石子(石子數可以爲0),將這兩堆石子的sg值異或,即爲取m個石子之前的sg值。
不過需要注意的一點是取了m個之後變成n-m元鏈,要轉換先後手。

#include <bits/stdc++.h>
using namespace std;
const int N=1010;
bool vis[N];
int t,n,m,sg[N];
void get_sg()//n>m時,打表sg函數
{
    memset(sg,0,sizeof(sg));
    for(int i=1;i<=n-m;i++)
    {
        memset(vis,0,sizeof(vis));
        for(int j=0;i-m-j>=0;j++)
            vis[sg[j]^sg[i-m-j]]=1;//左段長j,右段長i-m-j,總共長i-m
        for(int j=0;j<=n-m;j++)
            if(vis[j]==0){sg[i]=j;break;}
    }
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>t;
    for(int cas=1;cas<=t;cas++)
    {
        cin>>n>>m;
        printf("Case #%d: ",cas);
        if(n==m)printf("aekdycoin\n");//先手勝
        else if(n<m)printf("abcdxyzk\n");//後手勝
        else//n>m時,打表sg函數
        {
            get_sg();
            if(sg[n-m])printf("abcdxyzk\n");//後手勝(轉換先後手)
            else printf("aekdycoin\n");//先手勝
        }
    }
    return 0;
}

hdu 5795 A Simple Nim

輸入的s[i]最大爲1e9,sg數組開不了這麼大,所以要打表sg函數找規律。

打表代碼:

#include <bits/stdc++.h>
using namespace std;
int sg[110];
bool vis[110];
void get_sg()
{
    memset(sg,0,sizeof(sg));
    for(int i=1;i<=100;i++)
    {
        memset(vis,0,sizeof(vis));
        for(int j=0;j<i;j++)//若選擇取石子,後繼節點的sg值爲sg[0]~sg[i-1]
            vis[sg[j]]=1;
        for(int j=1;j<i;j++)//若選擇把石子分成三堆,第一堆個數j,第二堆個數k,後繼節點的sg值爲sg[j]^sg[k]^sg[i-k-j]
            for(int k=1;k<i&&i-k-j>=1;k++)//注意是i-k-j>=1,不是>=0!
                vis[sg[j]^sg[k]^sg[i-k-j]]=1;
        for(int j=0;j<=100;j++)
            if(vis[j]==0){sg[i]=j;break;}
    }
}
int main()
{
    get_sg();
    for(int i=0;i<=100;i++)
        printf("%d %d\n",i,sg[i]);
    return 0;
}

在這裏插入圖片描述
AC代碼:

#include <bits/stdc++.h>
using namespace std;
int t,n,x,sum;
int main()
{
    ios::sync_with_stdio(false);
    cin>>t;
    while(t--)
    {
        cin>>n;sum=0;
        for(int i=1;i<=n;i++)
        {
            cin>>x;
            if(x%8==0)x--;
            else if(x%8==7)x++;//寫else if,不要寫if!
            sum^=x;
        }
        if(sum)printf("First player wins.\n");
        else printf("Second player wins.\n");
    }
    return 0;
}

hdu 2873 Bomb Game

二維sg函數打表。

題意:給定n*m的棋盤,棋盤中有炸彈,每進行一次操作炸彈炸一次,炸一次生成兩個炸彈,分別位於左方和上方(左或上是邊界則不生成),炸完之後原炸彈消失,兩人輪流操作,最後不能引爆的輸。

思路:

一維的情況,等價於多堆取石子的遊戲,sg值即石子數,本題中也就是到1,1的距離。

二維時,引爆每個炸彈後會產生兩個新的炸彈,而這個炸彈的sg即可看做新產生的兩個炸彈的sg的異或(即"NIM和"),這樣只要修改一下一維求sg的函數,變可以構造出二維的sg函數表,對應有炸彈的位置的sg值異或起來就可以判定勝負。

#include <bits/stdc++.h>
using namespace std;
char a[60][60];
bool vis[1010];//vis開大一點
int n,m,sum,sg[60][60];
int get_sg(int x,int y)
{
    memset(vis,0,sizeof(vis));
    for(int i=1;i<x;i++)
        for(int j=1;j<y;j++)
        vis[sg[x][j]^sg[i][y]]=1;
    for(int i=0;;i++)
        if(!vis[i])return i;
}
int main()
{
    ios::sync_with_stdio(false);
    for(int i=1;i<=50;i++)
        for(int j=1;j<=50;j++)
        {
            if(i==1)sg[i][j]=j-1;
            else if(j==1)sg[i][j]=i-1;
            else sg[i][j]=get_sg(i,j);//打表
        }
    /*for(int i=1;i<=50;i++)
        for(int j=1;j<=50;j++)
        j==50?printf("%3d\n",sg[i][j]):printf("%3d ",sg[i][j]);*/
    while(cin>>n>>m&&(n||m))
    {
        sum=0;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
            {
                cin>>a[i][j];
                if(a[i][j]=='#')sum^=sg[i][j];
            }
        if(sum)printf("John\n");
        else printf("Jack\n");
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章