选球博奕与动态规划(三)

     观察图一可发现,各局面在决策树中是反复出现的,而且较复杂的局面可以分解为较简单的局面。现将各种情况分开讨论如下:

选球博奕与动态规划(三)_第1张图片

                                                                           图                        二


         图二中,a、b、c、d分别对应着初始局面为1 球、2球、3球、4球的情况。按照游戏要求,首轮决策不能将所有球取完,因此,初始局面为1球时无解;初始局面为2球时,由于同样的原因,只能转化为1球局面,此时1球局面不再是初始局面,故可不再受此规则限制,而推进至终局面;同样,初始局面为3球时,它只能转化为1球或2球两种局面,此时,1球局面可以直接推至终局面,而2球局面除了可转化为1球局面外,由于不再是初始局面,故会增加一个直接推至终局面的分支,但由于受另一条游戏要求,后一轮决策所取球数不能超过前一轮决策的取球数,的限制,实际上此分支不可选;初始局面为4球时,出于同样原因,它只能推进为3球、2球或1球三种局面,此时1球局面与b中的推进情况相似,而由于前一轮决策所取球数为2,所以2球局面直接推至终局面的分支,在此时可选,另外,3球局面虽然有终局面、1球、2球三个子局面备选,但同样由于受 “后一轮决策所取球数不能超过前一轮决策的取球数” 的限制,实际上只能取1个球而将局面转化为1球局面。

        从以上讨论可知,游戏要求“后一轮决策所取球数不能超过前一轮决策的取球数”,实际上是在中间局面所有潜在可能的选择上作了一个限制。如果不考虑这一限制,那么各局面与所有可能潜在的子局面将形成一种依赖关系。

选球博奕与动态规划(三)_第2张图片

                                                     图                      三


         图三罗列出了各种局面与其所有可能潜在的子局面之前的依赖关系,这种依赖关系形成一种重叠结构,图中的6球是初始局面,根据之前的讨论,图三可以扩展为初始局面的总球数为任意值的情况。

        局面N的一个解可看作是众多备选答案中符合要求的一个答案,也就是说求局面N的一个解实际上可看作是求最优值的问题。而根据之前讨论得到的局面有解或无解的等价描述可知,一个局面的解一定包含在它的一个子局面中。也就是一个问题的最优值包含在它的子问题中。

        同时,根据图二、图三可以看到,求局面解的问题可分解为它的子问题,而这个过程会形成一种重叠结构。

        因此,取球博奕问题符合动态规划求解问题的特征,用动态规划的方法来解决是最适合的。

       根据之前的讨论可以得到求解局面N的递归公式A,如下:

       当 时有:

                   

       当  时有:

                     

       同时有以下关系成立:

           ,

         

          


       根据以上递归关系可以构建问题的动态规划解,为了保存动态规划过程中形成的中间结果,可以设定以下的一种存储结构(假设初始局面的总球数最大值为M):

struct SCond
{
    int  cond;
    bool  v;
    SCond*  pCndNext;
};

struct SNode
{
    int  n;
    SNode*  pNdNext;
};

struct SN
{
    SCond* pSCond;
    int  vol;
    SNode*  pSnNext;
};

SN  aySitu[M+1];


给出算法伪代码如下:

SOLVE(m, cnd)
  if aySitu[m].vol == cnd
     then return 1

  rm <- min(m, cnd)
  if aySitu[m] initiated 
    then return (int)anySitu[m].pSCond[rm].v
 
  aySitu[m].vol = m

  for c <- 1 to m-1     //condition
    do h <- 0  
       rear(aySitu[m].pSCond) <- create SCond
      
       for d <- 1 to c
         do h <- h + SOLVE(m-d,d)

       if h==c
         then  aySitu[m].pSCond[c].v = flase 
         else  aySitu[m].pSCond[c].v = true;
       aySitu[m].pSCond[c].cond = c
               
  return (int)aySitu[m].pSCond[m].v


        根据命题14可知,决策过程中,一个局面N的潜在可能子局面个数与N的局面值Vol(N)是相等的。同一个局面N可以出现在决策树中的不同位置,其局面条件(Cond(N))可能相同也可能不同(参见图二),如果其局面条件相同,根据命题15,局面针对当前决策者的解也相同。因此,每个局面只要构建一个所有条件下是否有解的查找表,在接下来的动态规划过程中,就可以以此为基础,方便地建立起所有局面的依赖关系(如图三所示)。以上结构中,SN用来保存一个局面,SN中的域SCond* 就是用来保存一条由SCond为结点的链表的,链表中的每个结点保存了某一局面条件下,当前局面是否有解的信息。

        对以上伪代码需要作以下几点说明:代码指针域在C++中应用“->” 来指向,而在以上的伪代码里,用“.”来表示;生成的新结点要插入当前链表的尾部时,一般需要创建工作指针,而此处仅用 rear(...) <- create SCond来表示;对于链表中某一指定位置c 的结点的访问,一般需要从链表的首结点开始,逐一计数,直到找到第c 个结点为止,此处直接用下标(如pSCond[c])来表示这一查找过程;在创建结点链表pSCond时,其位置值是与条件值一致的;局面所有球一次性被取走的情况不包括在pSCond链表中,因为局面在此种情况下的返回值,在实际的求解过程中,将由以上代码中的2-3行来执行。

        以上代码中,SCond中的cond域以及SN中的SNode域,没有被使用,这两个域是为代码的优化预留的。

        回到开头的问题:24个球,首轮决策者应该取几个能保证取到最后一个球?经以上算法处理后,如果aySitu[24].pSCond[23].v == true,那么说明局面有解;此时,检查其所有子局面G,1Vol(G) ≤ 23,如果某个子局面G无解,则说明G是局面24的解的首轮决策选择,将24减去G的值就可得到最终的答案;如果有多个子局面无解,说明局面24有多个解,将24逐一减去相应的G的值就可得到所有答案。


        以上的算法还需要作进一步的优化。       (待续)

你可能感兴趣的:(趣味编程集)