题目描述
思路解析
一句话答案:LeetCode 1025 除数博弈:从 n 轮流减真因子,谁没得减谁输。博弈 DP 记 dp[i]=轮到你能否赢,能走到对手必输的局面就赢,n 偶数先手必胜。时间 O(n²)、空间 O(n)。
除数博弈这道题,先手到底靠什么赢
黑板上写着数字 n,Alice 先手。每步选一个真因子 j(能整除 n、又比 n 小的正整数),把 n 换成 n-j。谁轮到时 n 已是 1、没因子可减,谁就输。两人都最优,问先手 Alice 能否赢,题面 n=10 时她赢、返回 true。难在「双方都最优」:得算到对手接手后会不会反过来治你。
照规则一步步模拟对局,为什么会越算越慢
照规则递归模拟:轮到你把每个能减的真因子都试一遍,看哪步能把对手逼进必输;对手接手也要挨个试他的因子,一层套一层,局面数随 n 指数级膨胀。更亏的是同一局面被反复算——8 减 2、9 减 3 都落到 6,「面对 6 会怎样」被重判多次。病根是子局面重复计算。
从最小的局面倒推,dp[i] 记的到底是什么
既然子局面被反复算,就让每个只算一次、存下来,这就是动态规划(DP,把每个『面对数字 i 会怎样』的结论算一次存下、更大局面直接查)。定义 dp[i]:数字是 i、轮到你走能不能赢,能赢记 True、必输记 False。数字 1 没有更小的正因子可减,轮到谁谁走不了、直接输,dp[1]=False,是不必再往下拆的起点(base case,最简单、不用再推的情况)。从数字 2 起,每个 dp[i] 都由更小的已算局面推出。
存在一个因子能把对手推进必输,你就赢
面对数字 i,减掉真因子 j 后轮到对手:只要有一个 j 让 dp[i-j]=False(对手必输),你就赢定了,找到一个就够;反过来,每个因子减完落点 dp[i-j] 全是 True,你怎么走都是拱手让人,dp[i]=False。参考代码内层枚举 j,i%j==0 且 not dp[i-j] 一成立就置 dp[i]=True 并 break。
拿 n=10 把整张表从左往右填出来
一行格子,列号就是数字,先钉死 dp[1]=False。dp[2]:真因子只有 1,减 1 落到 dp[1]=False 对手必输,dp[2]=True(题面 n=2 返回 true)。dp[3]:减 1 落到 dp[2]=True、无别的因子可挑,dp[3]=False(n=3 返回 false)。dp[4]:减 1 落到 dp[3]=False,dp[4]=True。此后偶数格都能减 1 落到前一个必输的奇数、全为 True;奇数格减完只能落到能赢的偶数、全为 False。到 dp[10]=True,n=10 先手赢。
dp[1] 填错,为什么整张表会全反
dp[1] 手滑填成 True,dp[2] 会误判「减 1 落到对手能赢的局面」而算成 False,一路错到底、整张表全反,所以 dp[1]=False 这颗种子必须先钉死。复杂度上外层从 2 扫到 n、内层枚举因子最多 O(n) 个(大 O 记号形容规模变大时操作数大概怎么涨),合起来 O(n²),空间 O(n)。另一坑是内层 j 只能取 0<j<i 的真因子,把 j=i 也算进去就减到 0、下标越界。本题另有 O(1) 规律解:n 偶数先手必赢、return n%2==0,但它为什么成立仍靠这张表。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心一句话:dp[i]=轮到你走能否赢;能走到一个让对手必输(✗)的局面你就赢(✓)。dp[1]=✗。下面一步步演给你看。
dp 表一行,列号 = 当前数字 i(0..10)。✓=轮到的人必胜,✗=必输。从最小的 i=1 开始填。
i=1 时已经没有 0<j<1 的因子可减,轮到谁谁就走不了,所以 dp[1]=✗(必输)。
算 dp[2]:能减的真因子有 1。看落点——减 1→1(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[2] 就是 ✓。
存在因子 1:减掉后变成 1,而 dp[1]=✗ 是对手必输。所以 dp[2]=✓(轮到你你赢)。
算 dp[3]:能减的真因子有 1。看落点——减 1→2(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[3] 就是 ✓。
每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[3]=✗(必输)。
算 dp[4]:能减的真因子有 1、2。看落点——减 1→3(dp=✗),减 2→2(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[4] 就是 ✓。
存在因子 1:减掉后变成 3,而 dp[3]=✗ 是对手必输。所以 dp[4]=✓(轮到你你赢)。
算 dp[5]:能减的真因子有 1。看落点——减 1→4(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[5] 就是 ✓。
每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[5]=✗(必输)。
算 dp[6]:能减的真因子有 1、2、3。看落点——减 1→5(dp=✗),减 2→4(dp=✓),减 3→3(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[6] 就是 ✓。
存在因子 1:减掉后变成 5,而 dp[5]=✗ 是对手必输。所以 dp[6]=✓(轮到你你赢)。
算 dp[7]:能减的真因子有 1。看落点——减 1→6(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[7] 就是 ✓。
每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[7]=✗(必输)。
算 dp[8]:能减的真因子有 1、2、4。看落点——减 1→7(dp=✗),减 2→6(dp=✓),减 4→4(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[8] 就是 ✓。
存在因子 1:减掉后变成 7,而 dp[7]=✗ 是对手必输。所以 dp[8]=✓(轮到你你赢)。
算 dp[9]:能减的真因子有 1、3。看落点——减 1→8(dp=✓),减 3→6(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[9] 就是 ✓。
每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[9]=✗(必输)。
算 dp[10]:能减的真因子有 1、2、5。看落点——减 1→9(dp=✗),减 2→8(dp=✓),减 5→5(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[10] 就是 ✓。
存在因子 1:减掉后变成 9,而 dp[9]=✗ 是对手必输。所以 dp[10]=✓(轮到你你赢)。
最右 dp[10]=✓,n=10 是偶数 → Alice 必胜。看整行:偶数列全是 ✓、奇数列全是 ✗——这就是「偶数赢」的规律,而 DP 这套写法对任何博弈转移都通用。
小数边界先想清。
两个高频追问。
参考代码
def divisorGame(n: int) -> bool: dp = [False] * (n + 1) # dp[1]=False for i in range(2, n + 1): for j in range(1, i): if i % j == 0 and not dp[i - j]: dp[i] = True break return dp[n]复杂度
- 时间:O(n²),每格 i 枚举它的因子 j,最坏 O(n)
- 空间:O(n),dp 表一行 n+1 格
易错点
面试追问把动画讲成自己的话
追问能不能不用 DP?
追问这类博弈 DP 的通用套路是什么?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
泰波那契数
LeetCode 1137 · 简单 · 沿着 动态规划套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题