首发于松鼠的窝
10170 Sprague-Grundy定理是怎么想出来的

10170 Sprague-Grundy定理是怎么想出来的

  最近接触了两个双人组合博弈游戏。它们的解法都用到了Sprague-Grundy定理(见文末),利用这个定理,可以写出高效的记忆化搜索程序,以计算必胜策略。Sprague-Grundy定理如神来之笔一般为游戏的每个状态定义了一个Sprague-Grundy数(简称SG数),又同样匪夷所思地指出游戏状态的组合相当于SG数的异或运算。想必所有人学到这个定理的时候,都会在脑中冒出两个大大的问号:SG数和异或运算都是怎么想出来的呢?这篇专栏,就来研究SG数提出的动机,以及异或运算的发现和证明。

一、游戏介绍

  第一个游戏来自刷题网站LeetCode,名叫Flip Game。它的规则如下:初始时有一个由加号(“+”)组成的字符串,例如“++++++”。游戏双方轮流进行如下操作:选取相邻的两个加号,把它们变成减号。若轮到某一方时,字符串中不再有相邻的两个加号,则这一方输掉游戏。

  这个游戏有一种等价表述,称为Dawson's Chess。它的规则为:初始时有一个一维棋盘;游戏双方轮流下子,要求新下的棋子不能与已有的棋子相邻;无处下子者判负。

  第二个游戏是由知乎网友@张健提出来的:如何才能确保在这样一个抢票对决中获胜? - 数学。它以抢电影票为背景,不过可以抽象出如下的规则:初始时有一个二维棋盘,其中有些格子已经被划掉。游戏双方轮流选取棋盘上一块不包含已划掉格子的矩形区域,并划掉其中的所有格子。划掉最后一个格子者为胜。

  与这两个游戏类似的,还有这一类游戏的代表:Nim。它的游戏规则为:初始时有若干堆石子,游戏双方轮流选定一堆并从中拿走任意颗石子(可以把一堆拿光),拿走最后一颗石子者为胜。

二、策梅洛定理

  对于上面这些游戏,我们很自然地会关心一个问题:给定一个初始状态,先手玩家是否有必胜策略?

  策梅洛定理(Zermelo's theorem)指出,若一个游戏满足如下条件:

    1. 双人、回合制;
    2. 信息完全公开(perfect information);
    3. 无随机因素(deterministic);
    4. 必然在有限步内结束;
    5. 没有平局;

则游戏中的任何一个状态,要么先手有必胜策略,要么后手有必胜策略(下文把这两种状态分别称为“胜态”、“败态”)。

  常见的牌类游戏大多不满足条件2、3;常见的棋类游戏(如井字棋、五子棋、围棋、象棋、跳棋)大多满足条件2、3,在正式竞技中也会通过禁止循环的方式保证条件4,但不一定满足条件5。而第一节中提出的三种游戏,满足全部5个条件。

  策梅洛定理的结论其实颇为显然。它的证明过程也就是必胜策略的构造过程:

    • 对于终局状态,根据游戏规则可以判定“先手者”(即面对此状态的玩家)的胜负;
    • 对于非终局状态A,可以考虑先手玩家走一步之后的所有可能状态(称为A的“次态”):若A的次态全都是胜态,则A本身就是败态;否则,A为胜态,且必胜策略就是在次态中选择一个败态留给对方。由于游戏会在有限步内结束,这个递归过程必然能够终止。

  根据策梅洛定理,可以很容易地使用记忆化搜索算法判断一个状态是胜态还是败态:

mem = {}
def win(A):                     # 判断状态A是否为胜态
    if A not in mem:
        if is_final(A):         # 若A为终局态
            mem[A] = rule(A)    # 根据游戏规则判断A的胜负
        else:                   # 若A为非终局态,则根据策梅洛定理判断其胜负
            mem[A] = not all(win(B) for B in next_states(A))
                                # next_states(A)返回A的所有次态
    return mem[A]

  需要注意,这里的“状态”是需要包含“下一步轮到谁”这一信息的。另外需要讨论一下游戏满足的第4个条件——策梅洛定理本身只要求游戏在有限步内结束,但如果要使用上面的记忆化搜索算法,则需要枚举一个状态的所有次态,这要求在游戏中每一步的可能走法数也是有限的。下文也只讨论游戏的总步数和每步的走法数都有限的情况,这种游戏称为“有限游戏”(finite game)。

三、游戏状态的组合

  有些读者可能已经发现了,在第一节提出的三种游戏中,有许多状态是等价的。例如在Flip Game中,两个加号(“++”)和三个加号(“+++”)就是等价的状态,因为它们都是走一步之后就无路可走;在抢票游戏中,一个2x3的矩形和一个3x2的矩形也是等价的状态。状态的等价有许多种原因,其中一种是因为两个状态都是同样一些相互独立的子状态的组合。这里,“相互独立”的意思是指玩家的任意一步行动都只能影响一个子状态。例如,在Flip Game中,“++++-++--”和“-++--++++”就是等价的状态,因为它们都是“2个加号”和“4个加号”这两个子状态的组合。而Nim游戏的状态天然就是一些子状态的组合,其中每堆石子是一个子状态。如果能够通过子状态的胜负推断出它们的组合(下文称为“母状态”)的胜负,那么就可以大幅减少记忆化搜索过程中需要考虑的状态数,提高搜索效率。

  在讨论由子状态胜负推断母状态胜负的方法之前,我想先指出第一节中三个游戏的另外三个共同特征。它们正是Sprague-Grundy定理成立的条件,也是下文所有讨论的前提。

    1. 游戏双方可以采取的行动是相同的。井字棋、五子棋、围棋、象棋、跳棋这些棋类游戏均不满足这个条件,因为游戏的双方只能下(或移动)己方的棋子。
    2. 游戏双方的胜利目标是相同的。常见的胜利目标包括把棋盘清空或填满,或者把棋子排成特定的形状。注意,如果双方的目标是把棋子排成不同的形状,则游戏不满足这个条件。

      满足上面两个条件的游戏称为impartial game,反之则称为partisan game。impartial game的状态中只需包含棋盘信息,partisan game的状态则还需包括“下面轮到谁”。正因为如此,partisan game的状态无法拆分成“相互独立”的子状态,因为玩家的每一步行动会影响到所有子状态中“下面轮到谁”的信息。
    3. 双方的胜利目标具体来说,是自己亲手达成终局状态,或者说走最后一步者为胜(术语称为normal play)。第一节中的三个游戏也都可以稍微修改规则,改成走最后一步者为负(术语称为misère play),但下文的讨论仅适用于normal play的情况。

  下面讨论状态的组合对胜负的影响。请温习一下胜态和败态的关键性质:经过一步行动,败态只能变成胜态,胜态可以(但不一定)变成败态。

  先看两个败态的组合。两个败态的组合还是败态。从后手玩家的角度来看,先手玩家的行动只能将两个败态中的一个改变为胜态,于是后手玩家可以再将这个胜态变成败态,从而将两个败态的组合抛回给先手玩家。由于终局状态为败态,最终先手玩家必将面对两个终局状态组成的败态,故后手必胜。

  再看一胜一败两个状态的组合。胜态与败态的组合还是胜态——先手玩家只要把胜态变成败态,就可以把两个败态组合成的败态抛给后手玩家了。

  最后看两个胜态的组合。这种组合就比较复杂了:先手玩家不应把其中一个胜态变成败态,因为这样会把一胜一败两个状态组合成的胜态留给对方。因此,先手玩家应当把其中一个胜态变成一个新的胜态。后手玩家面对新的胜态+胜态的组合,应当采取相同的策略。然而,由于游戏是有限的,早晚会有一个玩家只能把一个胜态变成败态,从而输掉游戏,但我们并不知道这会在哪一步发生。也就是说,仅仅知道两个子状态都是胜态,不足以推出母状态的胜负;我们需要挖掘胜态的更多性质。

四、Sprague-Grundy数的提出

  我们以Flip Game为例,研究一下胜态还有什么更深入的性质。

  状态“++”是最简单的胜态,它只有一种走法,结果是败态。状态“+++”跟“++”在这一点上是一样的,因此它们其实是等价状态。状态“++++”就有两种不同的走法(对称的走法算同一种):一是把中间两个加号变成减号,这样得到的次态“+--+”是个败态;二是把某一端的两个加号变成减号,这样得到的次态“--++”或“++--”(等价于“++”)是个胜态。

  于是我们发现了两种不同的胜态。像“++”、“+++”这样,只能变成败态的胜态,我们称之为“一级胜态”。像“++++”这样,可以变成败态,也可以变成一级胜态的胜态,我们称之为“二级胜态”。类似地,如果一个胜态可以变成败态,也可以变成1至n-1级的所有胜态,则我们称之为“n级胜态”。而败态可以称为“零级”。

  我们看一下胜态的组合是否与级数有关。两个一级胜态的组合是败态,因为先手玩家的任意一步行动都会将其中一个胜态变为败态,留给后手玩家的就是胜态与败态组合成的胜态。一个一级胜态与一个二级胜态的组合是胜态,因为先手玩家可以将二级胜态变为一级胜态,留给后手玩家的就是两个一级胜态组合成的败态。两个二级胜态组合成的胜态也是败态,因为先手玩家无论将其中一个二级胜态变成败态还是一级胜态,留给对方的组合都是胜态。

  我们似乎发现了一个规律:两个同级胜态的组合是败态,两个不同级胜态的组合是胜态。没错!考察两个同级胜态的组合,无论先手玩家如何降低其中一个胜态的级数(甚至将其变成零级的败态),后手玩家总可以将另一个胜态降到同一级,最终先手玩家将面对两个败态组合成的败态。而若先手玩家面对的是两个不同级的胜态,他就总可以将其中较高级的胜态降至与较低级的胜态同级,这样留给后手玩家的就是败态。

  上面对于胜态等级的定义有一个漏洞:如果一个胜态A可以变成败态或二级胜态,但不能变成一级胜态,那么它应该算一级还是三级呢?规律的证明过程同样也有一个漏洞:我们默认了一步行动只能让胜态的级数降低,那么能不能让胜态的级数升高呢?注意到规律证明过程的关键,在于如果要降低一个胜态的级数,则可以降低到任一级。于是我们就知道,上面的状态A应当定义为一级胜态。这导致胜态的级数可以升高,不过没关系,可以这样弥补规律证明的漏洞:在两个同级胜态的组合下,若先手玩家升高了其中一个胜态的级数,则后手玩家可以将它降回原级,这样两个同级胜态的组合仍是败态。

  通过定义胜态的级数,我们解决了两个胜态组合而成的母状态的胜负判定问题。事实上,我们定义的“级数”,就是传说中的Sprague-Grundy数(简称SG数)。SG数是一个从状态映射到非负整数的函数,它的形式化定义如下:

  ★ SG(A) = \text{mex} \{ SG(B) | A \rightarrow B \}

式中A、B代表状态,A \rightarrow B代表B是A的一个次态。mex是一个定义在集合上的函数,表示不属于集合的最小非负整数,它是minimum excludant的缩写。这个定义用通俗的语言表达,就是说一个状态的SG数,等于它的次态取不到的最小SG数。

五、状态组合时Sprague-Grundy数的运算规则

5.1 规则的发现

  有了SG数,我们就可以判断任意两个子状态组合成的母状态的胜负了。但是,如果一个母状态是由三个子状态组成的,怎么办?我们发现,仅仅判断两个子状态组合成的母状态的胜负是不够的,我们还需要求出母状态的SG数。在下文中,我们用a \oplus b = c表示SG数分别为a、b的两个子状态组合成的母状态的SG数为c,我们的目标,就是弄清\oplus运算的法则。当然,我们这么写默认了由子状态的SG数可以唯一确定母状态的SG数,这一点其实未经证明。

  依然从最简单的情况开始。两个败态的组合还是败态,也就是说0 \oplus 0 = 0。两个一级胜态的组合也是败态,即1 \oplus 1 = 0。一个败态和一个一级胜态的组合,我们可以考虑最简单的情况:败态是终局态,不能再改变;而一级胜态只能变成败态。显然,这个组合的次态只能是两个败态组成的败态,故它本身是一级胜态,即0 \oplus 1 = 1

  ——0 \oplus 0 = 01 \oplus 1 = 00 \oplus 1 = 1,聪明的读者,你看出规律了吗?

  如果你没看出规律,或者不相信你看出的规律,我们可以再算几个SG数稍微大一点儿的情况。考虑最简单的败态和二级胜态的组合:败态不能变化,二级胜态只能变成一级胜态或败态,于是组合的次态的SG数只能是0 \oplus 1 = 10 \oplus 0 = 0。这说明败态和二级胜态的组合是二级胜态,即0 \oplus 2 = 2。同理可得0 \oplus 3 = 3。再看胜态和胜态的组合。前面已经得到,两个同级胜态的组合为败态,故2 \oplus 2 = 3 \oplus 3 = 0。那么两个不同级胜态的组合呢?

    • 1 \oplus 2的次态可能是0 \oplus 2 = 21 \oplus 0 = 11 \oplus 1 = 0,次态的SG数中0、1、2俱全,故1 \oplus 2 = 3
    • 1 \oplus 3的次态可能是0 \oplus 3 = 31 \oplus 0 = 11 \oplus 1 = 01 \oplus 2 = 3,次态的SG数缺少2,故1 \oplus 3 = 2
    • 2 \oplus 3的次态可能是0 \oplus 3 = 31 \oplus 3 = 22 \oplus 0 = 22 \oplus 1 = 32 \oplus 2 = 0,次态的SG数缺少1,故2 \oplus 3 = 1

  现在看出规律了吗?相信了吗?

  我们再换个角度,看看我们已经得到了\oplus运算的哪些性质:

    1. 交换律a \oplus b = b \oplus a:显然;
    2. 结合律(a \oplus b) \oplus c = a \oplus (b \oplus c):显然;
    3. 归零律a \oplus a = 0:因为两个同级胜态的组合为败态;
    4. 恒等律0 \oplus a = a:本节已用最简单的情况说明。

具有这四个性质的二元运算是什么呢?是异或

  到此为止,我们通过举例的方法,发现了状态的组合对应着SG数的异或。不过,我们并没有证明通过子状态的SG数能够唯一确定母状态的SG数(即\oplus运算结果的唯一性),也没有证明异或是能够达到这个目的的唯一一种运算。下面,我们就通过SG数和状态组合的定义,证明状态的组合对应着SG数的异或,即SG(A+B) = SG(A) \oplus SG(B),其中加号表示状态的组合,\oplus号表示异或。

5.2 规则的证明

  由SG数的定义,有SG(A+B) = \text{mex} \{ SG(X) | (A+B) \rightarrow X \}。由状态组合中子状态的独立性,可知状态X必能拆成C+BA+D的形式,其中A \rightarrow CB \rightarrow D。于是有:

  ★ SG(A+B) = \text{mex} \left( \{ SG(C+B) | A \rightarrow C \} \cup \{ SG(A+D) | B \rightarrow D \} \right)

下面,我们想要把右边的SG(C+B)SG(A+D)换成SG(C) \oplus SG(B)SG(A) \oplus SG(D)。但这正是我们要证明的结论呀!怎么办呢?可以用数学归纳法。不过,由于SG数在游戏过程中可能会增加,不能按SG数本身的顺序来归纳。但是,由于游戏是有限的,游戏的所有状态可以进行拓扑排序,这个顺序的逆序可以用作归纳的顺序。这样,我们就可以放心地进行代换了:

  ★ SG(A + B) = \text{mex} \left( \{ SG(C) \oplus SG(B) | A \rightarrow C \} \cup \{ SG(A) \oplus SG(D) | B \rightarrow D \} \right)

下面要证明的,就是SG(A) \oplus SG(B)不属于右边两个集合中的任意一个,但比它小的正整数都属于两个集合中的某一个。为书写简便,用a,b,c,d代替SG(A), SG(B), SG(C), SG(D)

  先看a \oplus b本身。由SG数的定义,既然A \rightarrow CB \rightarrow D,故c \ne ad \ne b,故两个集合均不包含a \oplus b。再看比a \oplus b小的任意非负整数e。定义f = a \oplus b \oplus e,则用fa,b,e三者异或,至少使得其中一者减小(因为f非零,f的二进制表示中最高位的1必来自a,b,e三者之一,f与这一者异或会使它减小)。但这一者不会是e,因为f \oplus e = a \oplus b > e。不妨设这一者是a,即f \oplus a = e \oplus b < a。此时,可取A的一个次态C使得SG(C) = c = e \oplus b,由SG数的定义,这是一定能做到的。这就证明了\forall\, e < a \oplus be都属于右边两个集合之一,故SG(A+B) = SG(A) \oplus SG(B)

  (最后一段的证明思路来自维基百科Nimber词条)

六、Sprague-Grundy定理的完整表述

  下面给出Sprague-Grundy定理的完整表述:

  若一个游戏满足以下条件:
  1. 双人、回合制;
  2. 信息完全公开(perfect information);
  3. 无随机因素(deterministic);
  4. 必然在有限步内结束,且每步的走法数有限(finite);
  5. 没有平局;
  6. 双方可采取的行动及胜利目标都相同(impartial);
  7. 这个胜利目标是自己亲手达成终局状态,或者说走最后一步者为胜(normal play);
则游戏中的每个状态可以按如下规则赋予一个非负整数,称为Sprague-Grundy数:
  ★ SG(A) = \text{mex} \{ SG(B) | A \rightarrow B \}
(式中A、B代表状态,A \rightarrow B代表A状态经一步行动可以到达B状态,mex表示一个集合所不包含的最小非负整数)。SG数有如下性质:
  1. SG数为0的状态,后手必胜;SG数为正的状态,先手必胜;
  2. 若一个母状态可以拆分成多个相互独立的子状态,则母状态的SG数等于各个子状态的SG数的异或。

  利用Sprague-Grundy定理,可以将记忆化搜索的程序优化成如下形式:

mem = {}
def SG(A):                      # 求状态A的SG数
    if A not in mem:
        S = sub_states(A)       # sub_states(A)将A尽可能细致地拆分成子状态
        if len(S) > 1:          # A可以拆分,用子状态的异或求其SG数
            mem[A] = reduce(operator.xor, [SG(B) for B in S])
        else:                   # A不可拆分,根据定义求其SG数
            mem[A] = mex(set(SG(B) for B in next_states(A)))
                                # next_states(A)返回A的所有次态
                                # 注意这条语句蕴含了“终局态的SG数为0”
    return mem[A]

这段程序中的状态只包含棋盘信息,不包含“下面轮到谁”。其中mex函数的实现十分平凡,从略。


10833 更新:

  受这个问题启发,我加强了Sprague-Grundy定理中的两个条件:

  • 一是“有限”:原先我只要求“游戏在有限步内结束”,这次添加了“每步的走法数也有限”;
  • 二是“impartial”:原先我只要求“双方可采取的行动相同”,这次添加了“双方的胜利目标也相同”。

  另外,我补充了第二部分结尾处代码遗漏的递归终止条件。

编辑于 2018-01-13

文章被以下专栏收录