前端算法第一七五弹-除数博弈

153 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情

爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。

最初,黑板上有一个数字 n 。在每个玩家的回合,玩家需要执行以下操作:

  • 选出任一 x,满足 0 < x < n 且 n % x == 0 。
  • n - x 替换黑板上的数字 n

如果玩家无法执行这些操作,就会输掉游戏。

只有在爱丽丝在游戏中取得胜利时才返回 true 。假设两个玩家都以最佳状态参与游戏。

示例 1:

输入:n = 2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。

示例 2:

输入:n = 3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。

数学

  • n=1n=1 的时候,区间 (0,1)(0, 1) 中没有整数是 nn 的因数,所以此时 Alice\text{Alice} 败。
  • n=2n=2 的时候,Alice\text{Alice} 只能拿 11nn 变成 11Bob\text{Bob} 无法继续操作,故 Alice\text{Alice} 胜。
  • n=3n=3 的时候,Alice\text{Alice} 只能拿 11nn 变成 22,根据 n=2n=2 的结论,我们知道此时 Bob\text{Bob} 会获胜,Alice\text{Alice} 败。
  • n=4n=4 的时候,Alice\text{Alice} 能拿 1122,如果 Alice\text{Alice}11,根据 n=3n = 3 的结论,Bob\text{Bob} 会失败,Alice\text{Alice} 会获胜。
  • n=5n = 5 的时候,Alice\text{Alice} 只能拿 11,根据 n=4n=4 的结论,Alice\text{Alice} 会失败。
  • ......

会发现这样一个现象:nn 为奇数的时候 Alice\text{Alice}(先手)必败,nn 为偶数的时候 Alice\text{Alice} 必胜。

证明:

  1. n=1n=1n=2n=2 时结论成立。

  2. n>2n>2 时,假设 nkn \leq k 时该结论成立,则 n=k+1n = k + 1 时:

    • 如果 kk 为偶数,则 k+1k+1 为奇数,xxk+1k+1 的因数,只可能是奇数,而奇数减去奇数等于偶数,且 k+1xkk + 1 - x \leq k,故轮到 Bob\text{Bob} 的时候都是偶数。而根据我们的猜想假设 nkn\le k 的时候偶数的时候先手必胜,故此时无论 Alice\text{Alice} 拿走什么,Bob\text{Bob} 都会处于必胜态,所以 Alice\text{Alice}处于必败态。
    • 如果 kk 为奇数,则 k+1k+1 为偶数,xx 可以是奇数也可以是偶数,若 Alice\text{Alice} 减去一个奇数,那么 k+1xk + 1 - x 是一个小于等于 kk 的奇数,此时 Bob\text{Bob} 占有它,处于必败态,则 Alice\text{Alice} 处于必胜态。

综上所述,这个猜想是正确的。

动态规划

Alice\text{Alice} 处在 n=kn = k 的状态时,他(她)做一步操作,必然使得 Bob\text{Bob} 处于 n=m(m<k)n = m (m < k) 的状态。因此我们只要看是否存在一个 mm 是必败的状态,那么 Alice\text{Alice} 直接执行对应的操作让当前的数字变成 mmAlice\text{Alice} 就必胜了,如果没有任何一个是必败的状态的话,说明 Alice\text{Alice} 无论怎么进行操作,最后都会让 Bob\text{Bob} 处于必胜的状态,此时 Alice\text{Alice} 是必败的。

结合以上我们定义 f[i]f[i] 表示当前数字 ii 的时候先手是处于必胜态还是必败态,true\texttt{true} 表示先手必胜,false\texttt{false} 表示先手必败,从前往后递推,根据我们上文的分析,枚举 ii(0,i)(0, i)ii 的因数 jj,看是否存在 f[ij]f[i-j] 为必败态即可。

var divisorGame = function(n) {
         f = new Array[n + 5];

        f[1] = false;
        f[2] = true;
        for (let i = 3; i <= n; ++i) {
            for (let j = 1; j < i; ++j) {
                if ((i % j) == 0 && !f[i - j]) {
                    f[i] = true;
                    break;
                }
            }
        }

        return f[n];
    }