博弈学习(一) NIM + SG函数

(一).NIM博弈

1)游戏规则

通常的Nim游戏的定义是这样的:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。


2)分析
定义P-position和N-position,其中P代表Previous,N代表Next。直观的说,上一次move的人有必胜策略的局面是P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到move的人有必胜策略的局面是N-position,也就是“先手可保证必胜”。更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。
按照这个定义,如果局面不可能重现,或者说positions的集合可以进行 拓扑排序 ,那么每个position或者是P-position或者是N-position,而且可以通过定义计算出来。

3)结论
根据定义,证明一种判断position的性质的方法的正确性,只需证明三个命题: 1、这个判断将所有terminal position判为P-position;2、根据这个判断被判为N-position的局面一定可以移动到某个P-position;3、根据这个判断被判为P-position的局面无法移动到某个P-position。
第一个命题显然,terminal position只有一个,就是全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'不是一个合法的移动。证毕。
根据这个定理,我们可以在O(n)的时间内判断一个Nim的局面的性质,且如果它是N-position,也可以在O(n)的时间内找到所有的必胜策略。Nim问题就这样基本上完美的解决了。

(二) SG函数

1)mex运算
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
 
对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Garundy函数g如下:g(x)=mex{ g(y) | y是x的后继 }。

2)SG函数的性质
首先,所有的terminal position所对应的顶点,也就是没有出边的顶点,其SG值为0,因为它的后继集合是空集。然后对于一个g(x)=0的顶点x,它的所有后继y都满足 g(y)!=0。对于一个g(x)!=0的顶点,必定存在一个后继y满足g(y)=0。
SG(X)=0:代表当前状态为必败状态


(三)练习题:


problem one: HDU 4848 Fibonacci again and again

分析:

nim 博弈的简单变形,只需要预处理出(1,1000)的数的SG函数的值即可。

代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 1010;

int f[30];
int sg[maxn+1];
int vis[maxn+1];

void init(){
    f[0]=1,f[1]=1;
    for(int i=2;i<20;i++)
        f[i]=f[i-1]+f[i-2];
}

void solve(){
    init();
    memset(sg,0,sizeof(sg));
    for(int i=1;i<=1000;i++){
        memset(vis,0,sizeof(vis));
        for(int j=1;j<maxn&&f[j]<=i;j++)
            vis[sg[i-f[j]]]=1;
        for(int j=0;;j++){
            if(!vis[j]){
                sg[i]=j;
                break;
            }
        }
    }
}

int main()
{
    int n,m,p;
    solve();
    while(~scanf("%d%d%d",&n,&m,&p)){
        if(n==0&&m==0&&p==0)
            break;
        if(sg[n]^sg[m]^sg[p])
            puts("Fibo");
        else
            puts("Nacci");
    }
    return 0;
}

problem two:HDU 1907 John

分析:

nim博弈,注意全部为1的情况。

代码如下:

Code Render Status : Rendered By HDOJ C++ Code Render Version 0.01 Beta

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int maxn = 50;

int a[maxn];

int main()
{
    int t,n;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        int s=0,num=0;
        for(int i=0;i<n;i++){
            scanf("%d",&a[i]);
            s^=a[i];
            if(a[i]>1) num++;
        }
        if((s&&num)||(!s&&!num))
           puts("John");
        else
            puts("Brother");
    }
    return 0;
}


problem there :HDU 3032 Nim or not Nim?

分析:

nim博弈的变形,每次可以从一堆取若干,或者把一堆变成两堆。打表找SG函数的规律。

代码如下:

打表找规律的代码:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

const int maxn = 1000010;

int a[maxn];

int sg[maxn];

int get(int x){
    int vis[1000];
    memset(vis,0,sizeof(vis));
    if(sg[x]!=-1) return sg[x];
    for(int i=x-1;i>=0;i--)
        vis[get(i)]=1;
    for(int i=1;i<=x/2;i++){
        int ans = 0;
        ans^=get(i);
        ans^=get(x-i);
        vis[ans]=1;
    }
    for(int i=0;;i++){
        if(!vis[i]){
            sg[x]=i;
            break;
        }
    }
    return sg[x];
}

int main()
{
    memset(sg,-1,sizeof(sg));
    sg[0]=0;
    int n;
    while(~scanf("%d",&n)){
        for(int i=0;i<20;i++){
            printf("sg( %d ) = %d\n",i,get(i));
        }
    }
    return 0;
}

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;

int get(int x){
    if(x==0) return 0;
    if(x%4==0)
        return x-1;
    if(x%4==3)
        return x+1;
    return x;
}

int main()
{
    int t,n,x;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        int ans = 0;
        for(int i=0;i<n;i++){
            scanf("%d",&x);
            ans^=get(x);
        }
        if(ans) puts("Alice");
        else puts("Bob");
    }
    return 0;
}






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值