Sprague-Grundy定理理解

例题

来自刷题网站LeetCode “464.我能赢吗”

题目描述:
(1)在 “100 game” 这个游戏中,两名玩家轮流选择从 1 到 10 的任意整数,累计整数和,先使得累计整数和达到或超过100 的玩家,即为胜者。
(2)如果我们将游戏规则改为 “玩家不能重复使用整数” 呢?
(3)例如,两个玩家可以轮流从公共整数池中抽取从 1 到 15 的整数(不放回),直到累计整数和 >= 100。
(4)给定两个整数 maxChoosableInteger (整数池中可选择的最大数)和 desiredTotal(累计和),若先出手的玩家是否能稳赢则返回 true ,否则返回 false 。假设两位玩家游戏时都表现最佳

定理内容均来自 知乎

Sprague-Grundy定理(简称SG)

1. 满足下列条件:

  • 双人、回合制;
  • 信息完全公开;
  • 无随机因素;
  • 必然在有限步数内结束;
  • 没有平局;
  • 双方可采取的行动及胜利目标都相同;
  • 这个胜利目标是自己亲手达成终局状态,或者说走最后一步者为胜;

则游戏中的任何一个状态,要么先手有必胜策略(胜态),要么后手有必胜策略(败态)。

2. 必胜策略的构造过程

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

根据策海格 定理,使用记忆化搜索算法判断一个状态是胜态还是败态:

// 使用哈希表存放状态
unordered_map<int,bool> men;
// 判断状态A是否为胜态
bool win(A){
    if(!men.count(A)){
        if(isFinal(A)){   // 若A为终局
            men[A] = rule(A);  // 根据题目规则判断A的胜负
        }
        else {
            // 并非正确语句,只是为了方便理解
            // 判断A的所有次态是否是败态
            men[A] = (!win(for(auto B : nextStates(A))));
        }
    }
}

根据 Sprague-Grundy 定理满足条件,题目中的每个状态都可以按照下面规则赋予一个非负整数,称为 Sprague-Grundy 数:

  • SG(A) = mex {SG(B) | A -> B}
  • 式中:A,B表示状态,A->B代表A状态经一步行动可以到达B状态,mex表示一个集合所不包含的最小非负整数;

SG数的性质

  • SG数为0的状态,后手必胜;SG数为正的状态,先手必胜;
  • 若一个母状态可以拆分成多个相互独立的子状态,则母状态的SG数等于各个子状态的SG数的异或。

将记忆化搜索程序进行优化:

// 编写语言 python
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数
            // next_states(A)返回A的所有次态<注意这条语句蕴含了“终局态的SG数为0”
            mem[A] = mex(set(SG(B) for B in next_states(A)))
    return mem[A]