从力扣1025题“除数博弈”解析博弈论与动态规划解题思路
2026/9/1 21:24:21 网站建设 项目流程

最近在整理算法笔记时,翻到了力扣第1025题“除数博弈”。这道题乍一看是个游戏,规则简单,但很多朋友第一次做的时候,要么是硬着头皮去模拟游戏过程,要么是尝试找规律但心里没底。题目描述是:爱丽丝和鲍勃轮流玩游戏,初始时有一个数字N。轮到谁时,谁就选择一个x,满足0 < x < NN % x == 0,然后用N - x替换黑板上的数字N。如果轮到某个玩家时无法进行任何操作,则判该玩家输。

游戏规则清晰,但直接去模拟每一步的选择,对于稍大的N来说,状态空间会非常复杂。这其实是一个典型的“博弈论”问题,更具体地说,是“公平组合游戏”中的一种。这类问题往往有一个共同点:最终的胜负在游戏开始时,就已经由初始状态决定了,与玩家的具体操作无关。理解这一点,是解决这类问题的关键。

很多人会陷入一个误区,认为必须去穷举所有可能的游戏路径,才能判断胜负。但实际上,对于“除数博弈”这类游戏,我们真正需要关注的不是“怎么走”,而是“当前状态是必胜态还是必败态”。一旦掌握了这个核心,问题就会从一个复杂的模拟游戏,简化成一个清晰的数学或动态规划问题。

1. 先别急着写代码:理解“必胜态”与“必败态”

在公平组合游戏中(双方操作规则相同,且没有随机因素),任何一个状态都可以被定义为“必胜态”或“必败态”。

  • 必胜态 (N-position):当前玩家有办法通过一次操作,将游戏状态转移到一个“必败态”留给对手。
  • 必败态 (P-position):当前玩家所有可能的操作,都会将游戏状态转移到一个“必胜态”留给对手。

游戏的终止状态(即无法操作的状态)是必败态。因为轮到你了,你却无路可走,你输了。

对于“除数博弈”:

  • N = 1时,没有满足0 < x < 1的整数x,当前玩家无法操作,所以N=1必败态
  • 我们的目标是判断N初始为某个数时,先手玩家(爱丽丝)是否处于必胜态。

那么,如何判断一个N是必胜态还是必败态呢?推理过程如下:

  1. 如果当前N是必败态,那么爱丽丝(先手)就输了。
  2. 如果当前N是必胜态,那么爱丽丝就赢了。
  3. 判断N的状态,需要看是否存在一个xN的正因子且不等于N),使得N - x这个新状态是必败态
  4. 如果存在这样的x,那么爱丽丝就可以选择这个x,把必败态扔给鲍勃,自己稳操胜券。此时N是必胜态。
  5. 如果所有可能的x对应的N - x都是必胜态,那么无论爱丽丝怎么选,都会把必胜态留给鲍勃,自己就输了。此时N是必败态。

这个过程天然形成了一个递归或动态规划的求解思路。我们可以从小到大地推导出每个N的状态。

2. 从暴力递归到动态规划:理清状态转移

最直观的想法是写一个递归函数canWin(N),判断当前数字N是否必胜。

def canWin(N): # 基础情况:N=1时,无法操作,必败 if N == 1: return False # 遍历所有可能的操作x for x in range(1, N): if N % x == 0: # x是N的因子 # 如果存在一种操作,能让对手进入必败态,则当前必胜 if not canWin(N - x): return True # 所有操作都无法让对手必败,则当前必败 return False

这个递归解法逻辑正确,但存在大量的重复计算,效率极低。例如,计算canWin(10)时会计算canWin(9)canWin(8)...,而这些子问题在计算其他N时又会被重复计算。

这正是动态规划(DP)大显身手的地方。我们可以用一个数组dp来记录每个N的状态,dp[i]表示当数字为i时,当前玩家是否必胜(True/False)。

动态规划的思路:

  1. 定义状态dp[i]i从 1 到N
  2. 初始状态dp[1] = False(数字1,无法操作,必败)。
  3. 状态转移:对于i从 2 到N,我们需要判断dp[i]
    • 遍历所有i的因子x1 <= x < ii % x == 0)。
    • 如果存在一个x,使得dp[i - x] == False(即对手处于必败态),那么dp[i] = True(当前玩家必胜)。
    • 如果所有这样的x对应的dp[i - x]都是True,那么dp[i] = False(当前玩家必败)。
  4. 最终答案dp[N]即为先手玩家爱丽丝的胜负情况。

基于这个思路,我们可以写出清晰的动态规划解法:

class Solution: def divisorGame(self, N: int) -> bool: if N <= 1: return False # dp[i] 表示数字为i时,当前操作者是否必胜 dp = [False] * (N + 1) # dp[0]无用,从dp[1]开始 dp[1] = False # N=1,必败 for i in range(2, N + 1): # 遍历所有可能的因子x for x in range(1, i): if i % x == 0: # 如果存在一种操作,能让对手进入必败态,则当前必胜 if not dp[i - x]: dp[i] = True break # 找到一个必胜策略即可跳出循环 # 如果循环完整结束都没有找到必胜策略,dp[i]保持初始的False,即为必败 return dp[N]

这个解法的时间复杂度是 O(N²)(因为对于每个i,最坏要遍历i-1次),空间复杂度是 O(N)。对于力扣的题目限制(N <= 1000)完全足够,也清晰地展示了从游戏规则到DP状态转移的完整逻辑。

3. 跳出DP框架:发现数学规律的本质

动态规划解法已经足够好,但如果你在纸上多推导几个Ndp值,可能会发现一个有趣的模式:

  • N=1: False (必败)
  • N=2: True (必胜) -> 爱丽丝选x=1,N变成1(必败态)给鲍勃。
  • N=3: False (必败) -> 因子只有1,N-1=2(必胜态)给鲍勃。
  • N=4: True (必胜) -> 可以选x=1(给鲍勃3-必败态),也可以选x=2(给鲍勃2-必胜态)。聪明人选x=1。
  • N=5: False (必败) -> 因子只有1,N-1=4(必胜态)给鲍勃。
  • N=6: True (必胜) -> 可以选x=1(给5-必败态),选x=2(给4-必胜态),选x=3(给3-必败态)。选1或3都能赢。

观察一下,N为 2, 4, 6 时先手胜,为 1, 3, 5 时先手负。这似乎暗示:N为偶数时,先手(爱丽丝)必胜;当N为奇数时,先手必败。

为什么?这需要一点数学归纳的思维:

  1. 终极必败态N=1(奇数),无法操作,是公认的必败态。
  2. 奇数的因子:一个奇数的所有因子(除了1和自身)都是奇数吗?不,但一个奇数不可能被偶数整除(0除外)。因此,一个奇数N的所有真因子x必然都是奇数
  3. 奇 - 奇 = 偶:如果N是奇数,x也是奇数,那么N - x必然是偶数
  4. 关键推论:当N是奇数时,当前玩家(无论是谁)的任何合法操作,都会将一个奇数N变成一个偶数(N-x)留给对手。
  5. 反之,偶数的因子:一个偶数N至少有一个因子是 1(奇数),它可以选择x=1,那么N-1就变成了一个奇数留给对手。
  6. 游戏进程推演
    • 如果爱丽丝开局拿到偶数N,她总可以选择x=1,把一个奇数(N-1)扔给鲍勃。
    • 轮到鲍勃时,他面对一个奇数。根据第4点,他无论怎么操作,都只能还给爱丽丝一个偶数。
    • 如此循环,爱丽丝永远面对偶数,她永远有x=1这个“安全操作”可用,可以持续把奇数扔给鲍勃。
    • 而鲍勃永远面对奇数,他只能制造偶数给爱丽丝。
    • 数字N在严格递减(因为x>=1),最终,鲍勃会面对那个终极奇数1,无路可走,输掉游戏。

因此,整个游戏的胜负,在N确定的那一刻就决定了:偶数先手必胜,奇数先手必败。这是一个非常简洁优美的数学结论。

4. 从解题到掌握:博弈类问题的通用思考框架

“除数博弈”这道题的价值,远不止于记住“偶数赢奇数输”这个结论。它提供了一个处理一大类博弈问题的通用思考框架。当你再遇到类似“两人轮流操作,无法操作者输”的题目时,可以按以下路径分析:

第一步:识别游戏类型

  • 是否是“公平组合游戏”(Impartial Combinatorial Game)?即规则对双方是否完全对称,且无随机性。
  • “除数博弈”、“取石子游戏”(Nim)等都属于此类。

第二步:定义状态与胜负态

  • 将游戏局面定义为一个或多个状态变量(如“除数博弈”中的数字N)。
  • 明确最基本的“必败态”(Terminal Position),通常是无法进行任何合法操作的状态。
  • 用“必胜态/必败态”的逻辑去推理其他状态。

第三步:尝试寻找规律或状态转移

  • 方法A(通用):动态规划/记忆化搜索。这是最稳妥的方法,尤其当状态空间有限时。就像我们写的DP解法,定义dp[状态],从小状态开始递推或记忆化搜索大状态。
  • 方法B(高效):数学归纳/寻找规律。通过枚举小规模情况,观察胜负是否与状态的某个数学属性(奇偶性、模运算等)强相关。就像我们发现的奇偶规律。这往往是问题设计精巧之处,但DP是找到这个规律的有力工具。

第四步:验证与编码

  • 用DP验证你的数学猜想,或者直接用DP实现。
  • 最终代码可能极其简单(如return N % 2 == 0),但推导过程体现的是你对问题本质的理解。

回到“除数博弈”,它的最终Python解答简单到令人惊讶:

class Solution: def divisorGame(self, N: int) -> bool: # 偶数必胜,奇数必败 return N % 2 == 0

但请你务必明白,这个一行代码的背后,是博弈论的基本概念、动态规划的推导验证和数学归纳的深刻洞察。在面试或实际解决问题时,展示出从暴力模拟到DP优化,再到发现数学本质的完整思考链,远比直接抛出答案更有价值。

这类问题训练的不是记忆结论,而是将模糊的游戏规则转化为清晰的可计算状态模型的能力。这种能力,在解决更复杂的资源调度、策略选择等问题时,至关重要。所以,下次再看到类似的轮流操作题,别慌,先问问自己:这个游戏的“状态”是什么?“必败态”是什么?状态之间如何转移?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询