博弈:SG函数与SG定理

博弈论:https://blog.csdn.net/luomingjun12315/article/details/4547907

解题方法

  • 巴什博奕(Bash Game):有一堆n个物品,两人轮流从堆中取物品,每次取 x 个 ( 1 ≤ x ≤ m)。最后取光者为胜。给定n,m问先手胜还是败。
  •  solution:若n%(m+1)==0,则先手必败,否则必胜,因为他总可以把局面控制到m+1
  • 威佐夫博奕(Wythoff Game):有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
  • solution:设两堆较小的为a,大的为b,k=b-a则判断a_k=floor( k* (1 + sqrt(5))/2 )与a,若等,则必败。否则必胜。
  • 尼姆博奕(Nimm Game):有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
  • solution:奇异局势(必败):(a,b,c),有a^b^c=0,如(0,n,n)、(1,2,3)就是。从非奇异转奇异局势方法:可以对a,b,c任一个:a=b^c
  • SG(x)为0表示必败点,不为0表示必胜点,SG函数可解决大部分博弈问题,解题时只要给出模板中的f[]函数,即改变当前状态的操作方式。
  • 还可用dp解,利用必败点一定走向必胜点,必胜点一定有一种方法走到必败点。
  • 游戏和:等于两个子游戏的抑或和

奇异局势:必败。我们用(a[k],b[k])(a[k] ≤ b[k] ,k=0,1,2,...,n)( a[k] 其中 k 为下标 )表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。
 可以看出,a[0] = b[0] = 0,a[k]是未在前面出现过的最小自然数,而 b[k] = a[k] + k。

 那么我们要如何判断一个局势是否为奇异局势?公式如下:
          a[k] = [k(1+√5)/2](a[k]这个方括号为下标运算符,[k(1+√5)/2]这个方括号为取整运算符),b[k] = a[k] + k 。奇妙的是其中出现了黄金分割数(1+√5)/2 = 1.618...因此,由a[k],b[k]组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出 j = [a(√5-1)/2],若 a = [ j(1+√5)/2],那么a = a[j],b[j] = a[j] + j,若不等于,那么a = a[j]+1,b = a[j] + j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。

 有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
 
int main(){
    int a,b,k,a_k;
    while(scanf("%d%d",&a,&b)!=EOF){
         k = abs(a-b);
         a = a < b? a : b;
         a_k = floor(k*(1.0 + sqrt(5.0))/2);
         printf("%d\n",a!=a_k);
         //输出为0,说明该点为必败点,1为必胜点
    }
    return 0;
}

必胜点和必败点的概念:

       P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。

       N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。

必胜点和必败点的性质:

        1、所有终结点是 必败点 P 。(我们以此为基本前提进行推理,换句话说,我们以此为假设)

        2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。

        3、无论如何操作,必败点P 都只能进入 必胜点 N。

我们研究必胜点和必败点的目的为题进行时间简化,有助于我们的分析。通常我们分析必胜点和必败点都是以终结点进行逆序分析。我们以hdu 1847 Good Luck in CET-4 Everybody!为例:

当 n = 0 时,显然为必败点,因为此时你已经无法进行操作了

当 n = 1 时,因为你一次就可以拿完所有牌,故此时为必胜点

当 n = 2 时,也是一次就可以拿完,故此时为必胜点

当 n = 3 时,要么就是剩一张要么剩两张,无论怎么取对方都将面对必胜点,故这一点为必败点。

以此类推,最后你就可以得到;

      n    :   0    1    2    3    4   5    6 ...

position:  P    N   N    P   N   N   P ...

他们就是成有规律,使用了 P/N来分析问题变简单了。

Sprague-Grundy定理(SG定理):

        游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x。不知道Nim游戏的请移步:这里

SG函数:

        首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

        对于任意状态 x , 定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样 集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。即它的后继状态都是必胜点

//注意f一定要到1024
#include<bits/stdc++.h>
using namespace std;
#define far(i,t,n) for(int i=t;i<n;++i)
typedef long long ll;
typedef unsigned long long ull;
using namespace std;

int f[11]={1,2,4,8,16,32,64,128,256,512,1024};//f为状态函数
int SG[1010],S[1010];//S为SG的后继状态集合

void getSG(int n)
{
    memset(SG,0,sizeof(SG));
    for(int i=1;i<=n;++i)
    {
        memset(S,0,sizeof(S));
        for(int j=0;f[j]<=i&&j<11;++j)
            S[SG[i-f[j]]]=1;
        int k=0;
        while(S[k])
            ++k;
        SG[i]=k;
    }
}

int main()
{
    int n;
    getSG(1010);
    while(~scanf("%d",&n))
    {
        if(SG[n])
            printf("Kiki\n");
        else
            printf("Cici\n");
    }
}

【实例】取石子问题

有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?

SG[0]=0,f[]={1,3,4},

x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;

x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;

x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;

x=4 时,可以取走4-  f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;

x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;

以此类推.....

   x        0  1  2  3  4  5  6  7  8....

SG[x]    0  1  0  1  2  3  2  0  1....

由上述实例我们就可以得到SG函数值求解步骤,那么计算1~n的SG函数值步骤如下:

1、使用 数组f 将 可改变当前状态 的方式记录下来。

2、然后我们使用 另一个数组 将当前状态x 的后继状态标记。

3、最后模拟mex运算,也就是我们在标记值中 搜索 未被标记值 的最小值,将其赋值给SG(x)。

4、我们不断的重复 2 - 3 的步骤,就完成了 计算1~n 的函数值。

代码实现如下:

//f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理,如f[4]={1,2,4,8}
//SG[]:0~n的SG函数值
//S[]:为x后继状态的集合
int f[N],SG[MAXN],S[MAXN];
void  getSG(int n){
    int i,j;
    memset(SG,0,sizeof(SG));
    //因为SG[0]始终等于0,所以i从1开始
    for(i = 1; i <= n; i++){
        //每一次都要将上一状态 的 后继集合 重置
        memset(S,0,sizeof(S));
        for(j = 0; f[j] <= i && j <= N; j++)
            S[SG[i-f[j]]] = 1;  //将后继状态的SG函数值进行标记
         
        int k=0; //查询当前后继状态SG值中最小的非零值
        while(S[k])
            ++k;
        SG[i]=k;
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值