从SG函数浅谈解决博弈问题的通法

  基于笔者之前对于几种二元零和博弈游戏的介绍,这里将其思想进行简单的提炼,并引出解决这类二元零和博弈游戏的强大工具——SG函数。

  其实对于博弈游戏如Bash、Nim等基本类型,异或一些比较高级的棋类游戏例如井字棋、中国象棋、华容道等,可以说它们是同质的。

  我们先从比较高的角度来看待如何得到博弈当中最优的策略,这其实也是笔者认为解决简单的二元零和博弈、理解SG函数和写棋类AI关键所在。博弈是一个大量状态之间进行转移的过程,我们将每种状态视为一个节点,这种转移关系视为一种有向边,那么我们容易构建起博弈树(在SG函数中我们常称“游戏树”),我们是从结局(即叶节点,因为它的胜负态一目了然)往前搜索,然后逆着正常决策的过程,标记每种局面的胜负态,然后来帮助我们在按照正常决策顺序的时候,选出最优策略。对于华容道、五子棋、井字棋这类游戏,需要根据当前出现的局面,然后模拟出接下来可能出现的所有情况,然后评估棋局并回溯回来标记出最优策略。同样对于二元零和博弈,也是相同的道理。只不过二元零和博弈非常简单,其胜负态(我们用N标记面临当前局面的人有必胜策略,P是必败策略)常常会呈现出规律化的分布(这就是很多人常说的“找规律”,但是它没有体现“找规律”这种方法和博弈本身的关系),因此我们能够设计线性的算法来判断其胜负态。

  我们来举个例子来理解一下这个过程:

  ex1:给出一个nxm棋盘,将棋子从(1,m)开始移动,要求只能向左、下、左下移动,不能移动棋子的人输,请问分析这个游戏的必胜态分布。(Problem source :hdu 2147 )

  分析:对于这个问题,我们按照上面我们给出解决博弈问题的通法,从结局开始分析。

  对于这个矩阵(显然棋局可以看成矩阵),我们从(n,1)这个状态开始构建NP分布图,以4x4的矩阵为例,我们容易得到如下的NP分布图。

  N N N N

  P N P  N

  N N N N

  P N P  N

  规律就一目了然了。

 

  那么下面我们通过一个具体的问题来引出解决这一类二元零和博弈问题的通解——sg函数。其实从本质上讲,它就是一个打NP表的有力工具。

  ex2:给出三个石子堆的数目m、n、p,两个游戏玩家每次只能拿取斐波那契数个石子,最终没有石子取得人输,如何分析胜负态的分布?(Problem source : hdu 1848)

  有nim博弈基础读者会注意到,这其实是单堆nim的推广形式,既然是推广形式,就应该使用推广方法,即sg函数。

  上文提到,任何博弈都可以看成多个状态之间的转移,我们将每种状态视为一个顶点,而状态之间的转移视为点与点的有向边,这样我们容易建立起无环有向树,也就是我们常说的博弈树或者是状态树,而我们判断胜负态分布的关键就是从胜负态显然的叶节点开始,然后往根部构造sg函数。

  整体的思路明了了,我们如何具体的计算sg函数呢?

  这里定义一个运算符mex,对于整数集合mex(S),mex(S)的值是S集合中没有出现的最小负整数,那么对于sg(x),它记录着状态参数x(玩家当前面临单堆石子的剩余数)对应的胜负态,对于它的计算,有如下递推定义:

  sg(x) = mex({sg(y)|y∈son(x)}),其中son(x)表示状态树中状态参量x的儿子所有节点构成的集合。

  递推计算式给出的貌似有些唐突,我们从叶节点开始尝试模拟,对于x = 0的叶节点,按照上述定义,显然有sg[0] = 0.它其实对应着P态。而对于它的父节点x1,我们根据递推定义,sg(x1)必然不为0,也很容易理解它对应着N态,结合二元零和博弈胜负态的交替规律(这个规律很多资料中视为定理进行表述,对于理解整个决策过程非常重要),我们能够得到整个博弈树顶点的权值,即sg函数。

  我们还会得到结论,对于状态x,sg(x) = 0,面临这个状态参量x的游戏者必败。(可见很多资料往往“先手必胜”的说法并不准确。)

  当sg(x) != 0,面临这个状态参量x的游戏者必胜。

  其实可能有读者已经会疑惑了,这里sg(x)储存的值其实只有两种形态(0和非0),那么我们储存那些非零的数还有什么意义呢?

  刚好这与我们下面要解决的问题是呼应的。

  上文讨论了单堆取石子游戏的推广形式如何用sg函数来解决,那么对于ex2中的三堆含m、n、p个的石子(称为多个sg函数组合起来的组合游戏),基于对运算符mex和sg函数自身内涵的理解,面对sg(m) = k,我们可将其视为从含k个石子堆拿出任意数量的游戏操作(有人可能会质疑是否会面临sg(m) = mex({0,1,2,...,k,k+2})这种使得转化不等价的局面,其实可以实践一下,对于状态m是否会出现sg(y) = k + 2)。那么取石子游戏的推广类型就利用sg函数得到了完美解决.

  简单的参考代码如下:

  

#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1005;
int sg[maxn] , flag[maxn];
int f[21];

void get_sg(int n)
{
     sg[0] = 0;
       for(int i = 1;i <= n;i++)
       {

                memset(flag , 0 , sizeof(flag));
                for(int j = 1;i - f[j] >= 0;j++)
                       flag[sg[i - f[j]]] = 1;
                for(int k = 0;;k++)
                {
                     if(flag[k] == 0)
                      {
                         sg[i] = k;
                         break;
                      }
                }
       }
}
int main()
{
    f[1] = 1 , f[2] = 2;
    for(int i = 3;i <= 21;i++)
        {
          f[i] = f[i-1] + f[i-2];
         // printf("%d\n",f[i]);
        }
    get_sg(1001);
    int m , n , p;
    while(scanf("%d%d%d",&m,&n,&p) != EOF)
        {
            if(n == 0 || m == 0 || p == 0)
                  break;
           if((sg[m]^sg[n]^sg[p]) == 0) //注意位运算和==运算符的优先级
              printf("Nacci\n");
           else
              printf("Fibo\n");
        }
}

 

posted on 2016-06-17 09:37  在苏州的城边  阅读(585)  评论(0编辑  收藏  举报

导航