除数博弈 图解题解
这道题到底在问什么
- 输入
- n=10
- 输出
- true(Alice 赢)
最优解:为什么这么做
一句话答案: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,但它为什么成立仍靠这张表。
▶ 动画逐步走查(共 22 步)——想跟着动画一帧帧对照就展开
- 3核心一句话:dp[i]=轮到你走能否赢;能走到一个让对手必输(✗)的局面你就赢(✓)。dp[1]=✗。下面一步步演给你看。
- 4dp 表一行,列号 = 当前数字 i(0..10)。✓=轮到的人必胜,✗=必输。从最小的 i=1 开始填。
- 5i=1 时已经没有 0<j<1 的因子可减,轮到谁谁就走不了,所以 dp[1]=✗(必输)。
- 6算 dp[2]:能减的真因子有 1。看落点——减 1→1(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[2] 就是 ✓。
- 7存在因子 1:减掉后变成 1,而 dp[1]=✗ 是对手必输。所以 dp[2]=✓(轮到你你赢)。
- 8算 dp[3]:能减的真因子有 1。看落点——减 1→2(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[3] 就是 ✓。
- 9每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[3]=✗(必输)。
- 10算 dp[4]:能减的真因子有 1、2。看落点——减 1→3(dp=✗),减 2→2(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[4] 就是 ✓。
- 11存在因子 1:减掉后变成 3,而 dp[3]=✗ 是对手必输。所以 dp[4]=✓(轮到你你赢)。
- 12算 dp[5]:能减的真因子有 1。看落点——减 1→4(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[5] 就是 ✓。
- 13每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[5]=✗(必输)。
- 14算 dp[6]:能减的真因子有 1、2、3。看落点——减 1→5(dp=✗),减 2→4(dp=✓),减 3→3(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[6] 就是 ✓。
- 15存在因子 1:减掉后变成 5,而 dp[5]=✗ 是对手必输。所以 dp[6]=✓(轮到你你赢)。
- 16算 dp[7]:能减的真因子有 1。看落点——减 1→6(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[7] 就是 ✓。
- 17每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[7]=✗(必输)。
- 18算 dp[8]:能减的真因子有 1、2、4。看落点——减 1→7(dp=✗),减 2→6(dp=✓),减 4→4(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[8] 就是 ✓。
- 19存在因子 1:减掉后变成 7,而 dp[7]=✗ 是对手必输。所以 dp[8]=✓(轮到你你赢)。
- 20算 dp[9]:能减的真因子有 1、3。看落点——减 1→8(dp=✓),减 3→6(dp=✓)。只要其中有一个落到 ✗(对手必输),dp[9] 就是 ✓。
- 21每个因子减完,落点都是 ✓(对手必胜),自己没有翻盘走法,所以 dp[9]=✗(必输)。
- 22算 dp[10]:能减的真因子有 1、2、5。看落点——减 1→9(dp=✗),减 2→8(dp=✓),减 5→5(dp=✗)。只要其中有一个落到 ✗(对手必输),dp[10] 就是 ✓。
- 23存在因子 1:减掉后变成 9,而 dp[9]=✗ 是对手必输。所以 dp[10]=✓(轮到你你赢)。
- 24最右 dp[10]=✓,n=10 是偶数 → Alice 必胜。看整行:偶数列全是 ✓、奇数列全是 ✗——这就是「偶数赢」的规律,而 DP 这套写法对任何博弈转移都通用。
⚠️ 容易写错的地方
✗ 错:j 可以等于 n
✓ 对:要求 0<j<n,真因子不含 n 自己
j<i 才算合法走法
✗ 错:dp[1] 当成赢
✓ 对:dp[1]=✗,剩 1 走不了是输
没有真因子可减
✗ 错:只看一个因子就下结论
✓ 对:存在任一让对手输的走法即赢
是「存在」不是「所有」
完整代码(Python / C++ / Java)
Python
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]C++
bool divisorGame(int n){
vector<bool> dp(n + 1, false);
for(int i = 2; i <= n; ++i)
for(int j = 1; j < i; ++j)
if(i % j == 0 && !dp[i - j]){
dp[i] = true; break;
}
return dp[n];
}Java
public boolean divisorGame(int n){
boolean[] dp = new boolean[n + 1];
for(int i = 2; i <= n; i++)
for(int j = 1; j < i; j++)
if(i % j == 0 && !dp[i - j]){
dp[i] = true; break;
}
return dp[n];
}复杂度
时间
O(n²)
每格 i 枚举它的因子 j,最坏 O(n)
空间
O(n)
dp 表一行 n+1 格
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 除数博弈 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
这题不是有「n 偶数就赢」的规律吗,为什么还要写 DP?+
规律解确实更快,直接 return n%2==0 就是 O(1)。但「n 偶数先手必赢」这个结论本身,正是上面 DP 表推出来的——偶数格全 True、奇数格全 False 是算完整张表才浮现的规律。它为什么成立也能顺着表说清:偶数总能减 1 变成奇数,把对手推进必输局面;奇数的真因子必是奇数,奇数减奇数得偶数,任何减法都把对手送进必胜的偶数局面。面试里只甩规律容易被追问「凭什么」,DP 给的是可验证的推导。
dp[i] 为什么找到「一个」让对手必输的因子就够,不用把所有因子都比一遍?+
因为博弈里你只需要一条通往胜利的路。只要存在某个真因子 j 让 dp[i-j]=False,你照它走、把对手摁进必输局面,剩下的因子好不好都无所谓,所以参考代码里 not dp[i-j] 一成立就 break。反过来,判定 dp[i]=False 才需要确认「所有」因子减完落点都是 True、一个能赢的出路都没有。找存在用「有一个就行」,判全输用「全都不行」。
dp 数组开成 n+1 格,dp[0] 用得上吗?+
用不上,但开着无妨。真因子 j 满足 0
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 除数博弈 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
把这道题真正拿下
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。