news 2026/9/9 7:15:21

除数博弈:从动态规划到奇偶性数学规律的算法优化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
除数博弈:从动态规划到奇偶性数学规律的算法优化

这类题目最值得先看的不是解法本身,而是它背后的数学规律。很多人一看到“博弈”两个字,就想着去模拟整个游戏过程,用递归或者动态规划去穷举所有可能,这当然能解,但往往不是最优解,尤其是在面试的紧张环境下。除数博弈(LeetCode 1025题)就是一个典型例子,它表面上是一个游戏模拟题,实际上是一个可以用数学归纳法秒杀的找规律题。如果你正在准备算法面试,或者想提升自己快速识别问题本质的能力,这篇文章会带你从最直接的模拟思路开始,一步步推导出那个“看一眼就知道答案”的数学结论,并解释为什么这个结论成立。

我建议你先别急着看答案,花一分钟想想:如果给你一个数字n,你和对手轮流操作,每次选择一个能被n整除的x0 < x < n),然后将n替换为n - x。无法操作者输。假设双方都绝对聪明,你先手,你能赢吗?

下面,我会按照实际解题和思考的顺序,拆解这个问题。

1. 先理解规则,别急着写代码:问题重述与关键点

拿到任何题目,第一步永远是准确理解题意,找出所有约束和隐含条件。对于除数博弈:

  • 游戏状态:当前数字n
  • 操作规则:轮到的一方必须选择一个整数x,满足:
    1. 0 < x < n
    2. n % x == 0xn的除数,且不能是n本身)。
  • 状态转移:执行操作后,数字变为n - x
  • 终止条件:如果轮到某人时,n == 1,那么他无法找到满足条件的x(因为0 < x < 1的整数不存在),所以他输掉游戏。
  • 目标:假设你和对手都采取最优策略。给定初始数字n,你先手,返回true如果你能赢,否则返回false

这里有几个关键点容易被忽略,但直接影响解题思路:

  1. “最优策略”:意味着双方每一步都会选择让自己最终获胜的操作。在博弈论中,这通常引导我们使用动态规划或递归,从终局倒推。
  2. 操作改变的是n,不是x。你选了一个除数xn就变成n-x。这个新数字可能不再有之前那些除数。
  3. n的范围:根据题目描述,1 <= n <= 1000。这个范围不大,意味着即使使用O(n^2)的算法也完全可行,这给了我们使用动态规划的信心。

所以,最直接的思路就是模拟这个博弈过程,计算所有可能的状态。我们先从这个“笨办法”开始,它虽然慢,但能保证正确,并且是理解更优解法的基础。

2. 从“暴力”到清晰:递归与动态规划解法

2.1 递归思路(带记忆化)

我们可以定义一个函数canWin(n),表示在当前数字为n且轮到当前玩家操作时,当前玩家是否能赢。

  • 基础情况:如果n == 1,当前玩家没得选,直接输,返回false
  • 递归过程:对于当前的n,遍历所有可能的xn的除数,且1 <= x < n)。如果存在某个x,使得在对手面对n - x时他会输(即canWin(n - x) == false),那么当前玩家选择这个x就能迫使对手进入必败状态,因此当前玩家能赢,返回true。如果遍历完所有x,对手面对每一个n - x都能赢(即canWin(n - x) == true),那么当前玩家无论怎么选都会让对手进入必胜状态,所以当前玩家必输,返回false

这就是一个典型的“极小化极大”思路。直接递归会有大量重复计算,所以需要加上记忆化(Memoization)。

from functools import lru_cache class Solution: def divisorGame(self, n: int) -> bool: @lru_cache(maxsize=None) def canWin(current_n): # 当前玩家面对数字 current_n if current_n == 1: return False # 没得选,输 # 寻找所有可能的除数 for x in range(1, current_n): if current_n % x == 0: # 如果存在一个选择,能让对手面对必败局面,则当前玩家赢 if not canWin(current_n - x): return True # 所有选择都会让对手赢,则当前玩家输 return False return canWin(n)

这个解法逻辑正确,对于n=1000也能在要求时间内通过。但它不是最高效的,因为它隐藏了一个更简单的规律。

2.2 动态规划(递推)思路

我们可以用动态规划自底向上地计算。定义dp[i]为:数字i时,先手玩家是否能赢。

  • dp[1] = False(先手直接输)
  • 对于i > 1,我们需要遍历i的所有除数j1 <= j < ii % j == 0)。如果存在一个除数j,使得dp[i - j] == False(即对手在i-j时必输),那么先手玩家选择j就能赢,所以dp[i] = True。否则,dp[i] = False
class Solution: def divisorGame(self, n: int) -> bool: if n == 1: return False # dp[i] 表示数字为 i 时,先手是否能赢 dp = [False] * (n + 1) dp[1] = False # 基础情况 for i in range(2, n + 1): # 检查 i 的所有除数 for j in range(1, i): if i % j == 0: # 如果存在一个选择 j,能让对手(dp[i-j])处于必败,则当前先手赢 if not dp[i - j]: dp[i] = True break # 找到一个必胜策略即可 return dp[n]

运行这个 DP 解法,并打印出前几个n的结果,你会发现一个有趣的规律:

n=1: False n=2: True n=3: False n=4: True n=5: False n=6: True n=7: False n=8: True ...

看起来,当n是偶数时,先手(Alice)赢;当n是奇数时,先手输。真的是这样吗?我们来验证一下n=9。根据 DP,dp[9]会是False吗?手动推一下:9的除数有1, 3

  • 如果 Alice 选1n变为8dp[8]=True(Bob 面对 8 是必胜的),对 Alice 不利。
  • 如果 Alice 选3n变为6dp[6]=True(Bob 面对 6 是必胜的),也对 Alice 不利。 所以dp[9]确实是False。规律似乎成立。

3. 数学归纳与证明:为什么偶数必胜,奇数必败?

现在我们从数学上证明这个观察到的规律。这不仅能让你记住结论,更能锻炼你的数学归纳和博弈分析能力。

命题:对于除数博弈,初始数字为n,双方最优策略下,先手玩家获胜当且仅当n是偶数。

证明(使用数学归纳法):

  1. 基础情况

    • n = 1:奇数,先手无法操作,输。命题成立。
    • n = 2:偶数,先手只能选择x=1,将n变为1。后手面对1必输。所以先手赢。命题成立。
  2. 归纳假设:假设对于所有k < n,命题成立。即:k为偶数时先手赢,k为奇数时先手输。

  3. 归纳步骤:考虑n

    • 情况 A:n是奇数。 奇数n的所有除数x都是奇数(因为如果x是偶数且能整除奇数n,那么n/x会是分数,矛盾)。所以,对于任何合法的xn - x= 奇数 - 奇数 = 偶数。 根据归纳假设,对手面对偶数n-x时是必胜的。因此,无论先手选择哪个x,都会将局面变成一个对手必胜的偶数局面。所以,当n是奇数时,先手必败。
    • 情况 B:n是偶数。 偶数n至少有一个除数x=1(它是奇数)。先手可以选择x=1,那么n变为n-1,这是一个奇数。 根据归纳假设,对手面对奇数n-1时是必败的。因此,先手可以通过选择x=1,强制将局面变成一个对手必败的奇数局面。所以,当n是偶数时,先手有必胜策略(至少选择x=1就是必胜策略)。

    由归纳法,命题对任意正整数n成立。

核心洞见:这个证明揭示了游戏的关键——奇偶性。最优策略下,先手玩家如果拿到偶数,可以通过一直选择x=1,保证自己每次留给对手的都是奇数。而对手面对奇数时,无论怎么选(只能选奇除数),都会还回来一个偶数。如此循环,最终先手会将n=2的局面留给对手,自己获胜。

所以,最终的代码简单到令人发指:

class Solution: def divisorGame(self, n: int) -> bool: return n % 2 == 0

4. 从解题到举一反三:博弈类题目的常见套路与排查点

除数博弈提供了一个很好的范式:许多博弈问题看起来复杂,但可能存在简单的“必胜态/必败态”规律。处理这类题目时,我一般的排查和思考顺序是这样的:

4.1 判断问题类型

首先问自己:这是否是一个“公平组合游戏”?通常特征包括:

  • 两人轮流操作。
  • 完全信息(没有隐藏部分)。
  • 无随机因素。
  • 操作集合只依赖于当前状态,不依赖于玩家。
  • 无法操作者输(正常游戏规则)。 除数博弈完全符合。对于这类游戏,一个强大的工具是Sprague-Grundy 定理,但很多简单题目可以通过找规律或DP解决。

4.2 尝试小规模模拟与找规律

就像我们刚才做的那样,不要一上来就想复杂算法。用手算或写个简单的程序打印出n=1,2,3,4,5,6,7,8...的结果。观察规律:

  • 结果是否呈现周期性?(例如本题的奇偶性)
  • 是否和某个数学性质(质数、平方数、斐波那契数)相关?
  • 必败态(P-position)和必胜态(N-position)是否有递推关系?本题中,所有偶数都是N-position,所有奇数都是P-position

4.3 设计动态规划状态

如果规律不明显,DP是通用解法。关键是如何定义状态。

  • 状态:通常就是游戏的当前局面(如本题的n)。
  • 状态转移:从当前状态,枚举所有合法操作,到达下一个状态。如果存在一个操作能到达一个对手必败的状态,那么当前状态是必胜的;否则是必败的。
  • 初始化:确定游戏结束的终局状态(通常是无法操作的状态)是必败的。 本题的dp[i]定义就是经典范例。

4.4 优化与数学证明

找到规律(如偶数必胜)后,不要满足于“看起来对”。尝试用数学归纳法或反证法去证明它。证明过程能加深你对问题本质的理解。即使证明不严谨,在面试中说出清晰的推理思路,也比只背结论得分高得多。

4.5 常见踩坑点

  1. 忽略“最优策略”假设:题目说双方都聪明,意味着你要找的是“无论对手怎么应对,我都有办法赢”的策略,而不是模拟一两条随机路径。
  2. 递归/DP状态定义错误dp[i]必须明确是“当前轮到行动的玩家”的胜负,还是“先手玩家”的胜负。本题中,dp[i]表示“数字为i时,当前轮到的玩家(不一定是原始先手)的胜负”。在实现时,我们是从先手角度调用,逻辑是一致的。
  3. 遍历除数效率:在DP解法中,内层循环for j in range(1, i)效率是O(n),判断i % j == 0整体是O(n^2)。对于n<=1000可以接受。如果n很大,可以优化为只遍历jsqrt(i),但本题不需要。
  4. 误解题意操作:一定要看清操作是n = n - x还是n = n / x或者其他。一字之差,解法完全不同。

回到除数博弈,我们现在有了三种解法:1) 记忆化递归,2) 动态规划,3) 数学结论。在面试中,最理想的回答路径是:

  1. 阐述理解:复述题目,确认规则。
  2. 提出暴力/通用解法:“首先,我们可以用递归+记忆化或者动态规划来模拟所有可能。定义状态dp[i]...”
  3. 观察并优化:“在实现DP或者计算小样例时,我发现结果似乎只和奇偶性有关。偶数先手赢,奇数先手输。”
  4. 给出证明:“我们可以尝试证明一下:当n是奇数时,它的所有除数都是奇数,所以n-x是偶数,留给对手必胜态;当n是偶数时,我可以选择x=1,留给对手奇数,即必败态。因此结论成立。”
  5. 写出最终代码:给出return n % 2 == 0

这个思考过程,展示了你从暴力到优化、从现象到本质的完整能力链,远比直接背答案要强。

所以,下次遇到类似的博弈题目,比如LeetCode 292. Nim 游戏n % 4 != 0先手胜),或者LeetCode 877. 石子游戏(先手必胜),你就可以用类似的思路去分析:先从小数据找规律,再尝试用DP验证,最后思考能否数学归纳。这才是刷题提升的真正意义——掌握一类问题的解法,而不是一道题。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 8:07:18

Python越学越废?真正拖垮你的不是语法,是低效工作流

一、90%开发者都踩的致命误区 许多开发者都曾存有这样的困扰, 即尽力钻研语法, 使劲做足刷遍教程那般的功夫, 费尽心思吃透库函数, 然而写代码的速度以及准确率却一直没办法提升。明明自身基础特别扎实, 对于简单脚本, 正常情况下20分钟就能完成的事情, 自己做却要耗费3小时, …

作者头像 李华
网站建设 2026/9/6 13:26:07

格力后端笔试全解析:Java、MySQL与场景设计题备考指南

1. 格力的后端笔试题&#xff0c;到底在选什么样的人先交代一下背景。2020年秋季招聘&#xff0c;格力作为制造业头部企业&#xff0c;开启了规模不小的数字化人才招募&#xff0c;后端开发岗是其中核心方向之一。当年这波笔试在牛客和各类校招群里流传度很高&#xff0c;原因倒…

作者头像 李华
网站建设 2026/9/5 0:36:56

LeetCode 412 Fizz Buzz:从基础解法到可扩展设计的编程实践

这次我们来看一个经典的编程面试题&#xff1a;LeetCode 第 412 题 Fizz Buzz。这题本身不复杂&#xff0c;但它像一面镜子&#xff0c;能照出你写代码的基本功、对边界条件的处理&#xff0c;以及代码的可读性和扩展性。很多面试官喜欢用它来开场&#xff0c;因为它能快速判断…

作者头像 李华
网站建设 2026/9/5 8:03:38

Excel VBA正则表达式提取地址信息:高效拆分省市区详细地址

处理过Excel地址数据的人应该都有这种体会&#xff1a;一列原始地址看上去还算规整&#xff0c;但要按省、市、区、详细地址拆分时&#xff0c;手工复制粘贴费时费力&#xff0c;稍微量大一点就非常头疼。如果地址格式再乱一些&#xff0c;比如有的带“省”、有的不带&#xff…

作者头像 李华
网站建设 2026/9/6 10:47:32

技术视频数据优化:从推荐算法到动态视频策略

最近在折腾视频内容分发时&#xff0c;发现一个挺有意思的现象&#xff1a;一个看似“随意”发布的动态视频&#xff0c;其数据表现有时会远超精心策划的正式内容。这背后其实不是玄学&#xff0c;而是触动了平台推荐算法的某些“开关”。很多开发者&#xff0c;尤其是做技术分…

作者头像 李华
网站建设 2026/9/5 11:39:41

Coze多Agent实战:从拆分流程到掌控复杂任务的完整指南

前阵子我用 Coze&#xff08;扣子&#xff09;搭一个稍微复杂的小应用&#xff1a;用户提交需求&#xff0c;系统自动生成一份简历&#xff0c;再去匹配几个岗位方向。一开始我只做了一个智能体&#xff0c;让它“从头管到尾”。结果不到三轮就出了问题——它要么忘了前面用户填…

作者头像 李华