040 · Nim Game

algorithm
Published

June 18, 2026

My First Thoughts

这道题固定了我是先手,目标是给一个 n,判断我在双方都最优的情况下能不能赢。

例子其实已经给得比较明确了。先从小数字看:

n = 1, 2, 3

我都可以直接一次拿完,所以我赢。

到了:

n = 4

我就必败。因为我只能拿 123 个,剩下的石头一定会被对方一次拿完。

继续往后看:

n = 5, 6, 7

我又可以赢。因为我可以先拿走一些石头,把 4 留给对方。

再到:

n = 8

我又会输。

这个规律就比较明显了:关键数字是 4

可以这样理解:

到最后只剩下 4 个的时候,谁需要先取,谁就输。

换句话说,如果想让对方输,就尽量把 4 个石头留给对方。

更一般地看,如果 n4 的倍数,我就是必败的。因为不管我取多少个,假设我取了 m 个,对方只要每次取:

4 - m

这样两个人这一轮合起来正好取走 4 个。局面会一直回到 4 的倍数,最后就会轮到我面对 4 个石头。

同样的道理,只要 n 不是 4 的倍数,我就可以先取走那个余数,让剩下的石头数变成 4 的倍数。这样就把必败局面交给对方。

所以这题其实不是要模拟每一步,而是判断:

n 是不是 4 的倍数

如果可以选择先手或后手,那就是必胜:n4 的倍数时选后手,不是 4 的倍数时选先手。这样总能把必败局面交给对方。

但 LeetCode 这题固定我是先手,所以胜负才只取决于数字本身。每 4 个数里只有 4 的倍数是先手必败,其他三种余数都是先手必赢,所以固定先手时可以理解成先手赢面是 3/4


Why That Is Not Enough

这个思路已经完整了。剩下要做的只是把口头规律压成一个布尔表达式。

题目问的是:

我是否一定能赢

所以返回值应该是:

  • 如果 n4 的倍数,返回 False
  • 如果 n 不是 4 的倍数,返回 True

在 Python 里,“是不是 4 的倍数”可以用取余判断:

n % 4 == 0

因此“我能赢”就是它的反面:

n % 4 != 0

Final Idea

关键规律是:

只要我能把 4 的倍数留给对方,对方就在必败局面里。

如果 n 不是 4 的倍数,比如:

n = 5, 6, 7

我可以分别先拿走:

1, 2, 3

这样剩下的就是 4,交给对方。

如果 n 是更大的非 4 倍数,比如:

n = 10

我先拿走 2,剩下 8,还是 4 的倍数。

之后对方每次拿 m 个,我就拿 4 - m 个,让两个人一轮合起来拿走 4 个。这样局面一直保持在 4 的倍数上,最后对方会面对无法翻盘的局面。

反过来,如果一开始 n 就是 4 的倍数,我没有办法第一步还把 4 的倍数留给对方。无论我拿 12 还是 3 个,对方都能用同样的补数策略把局面重新变回 4 的倍数。

所以最终判断就是:

n % 4 != 0

Why It Works

每一回合最多可以拿 3 个石头,而两个人合起来可以被控制成一组 4

我拿 m 个,对方拿 4 - m 个

或者反过来:

对方拿 m 个,我拿 4 - m 个

因为 m 只能是 123,所以 4 - m 也一定是 123,是合法操作。

当某个人面对 4 个石头时:

  • 他拿 1,对方拿 3
  • 他拿 2,对方拿 2
  • 他拿 3,对方拿 1

对方总能拿完最后的石头。因此 4 是必败局面。

同理,81216 这些 4 的倍数,也都是必败局面。因为只要当前玩家打破这个倍数,对方就可以补回一组 4

所以:

  • n % 4 == 0:先手必败
  • n % 4 != 0:先手可以先拿走余数,把必败局面交给对方

Code

def canWinNim(n):
    return n % 4 != 0

Complexity

Time \(O(1)\) - 只做一次取余判断
Space \(O(1)\) - 不需要额外数据结构

Takeaway

博弈题不一定要模拟所有选择。先找小数字里的必败局面,再观察周期。如果每轮双方的操作可以被控制成固定总数,就可以用取余直接判断胜负。


Quiz