【博弈论】拿石头如何保证先手赢?

282 阅读4分钟

1.问题描述

你和你的朋友,两个人一起玩 Nim 游戏

  • 桌子上有一堆石头。
  • 你们轮流进行自己的回合, 你作为先手
  • 每一回合,轮到的人拿掉 1 - 3 块石头。
  • 拿掉最后一块石头的人就是获胜者。

假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false

示例1

输入:n = 4
输出:false 
解释:以下是可能的结果:
1. 移除1颗石头。你的朋友移走了3块石头,包括最后一块。你的朋友赢了。
2. 移除2个石子。你的朋友移走2块石头,包括最后一块。你的朋友赢了。
3.你移走3颗石子。你的朋友移走了最后一块石头。你的朋友赢了。
在所有结果中,你的朋友是赢家。

示例2

输入:n = 1
输出:true

示例3

输入:n = 2
输出:true

提示:

  • 1 <= n <= 231 - 1

2.记忆化搜索(超出内存限制)

在关注自己的时候,一定要关注对方。因为双方是都足够聪明。像本题,每个人每次都可以拿1-3块石头,因此就需要判断拿了1-3块石头,剩下的石头让对方做先手是否会输,但凡有一种情况会输,自己都会赢【因此需要把这三种情况都需要搜索一下】。

class Solution {
    public boolean canWinNim(int n) {
        // 包装类存在null这个状态,用null表示当前i的状态还没有被计算出来。
        Boolean[] memo = new Boolean[n];
        return dfs(n, memo);
    }

    boolean dfs(int n, Boolean[] memo) {
        // 小于等于3,最后拿的人一定会赢
        if(n <= 3) return true;
        boolean flag = false;
        if(memo[n] != null) return memo[n];
        // 有3种选择,只要有一种方案让对方输掉,自己就可以赢
        for(int i = 1; i <= 3; i++) {
            if(!dfs(n - i, memo)) {
                flag = true;
                break;
            }
        }
        memo[n] = flag;
        return flag;
    }
}

3.动态规划(超出内存限制)

普通的暴力递归有重复情况,因此可以用缓存做进一步优化【记忆化搜索】,而动态规划在遍历过程中就没有重复的情况,因此时间复杂度会小不少。其实改动态规划就是消重的过程。有可变参数代表得暴力递归就可以改成动态规划。此题无非就是将记忆化搜索改成动态规划,如何改呢?分为以下几个步骤

  • 确定dp[i]的含义:一共i块石头,先手拿的话是否可以赢。
  • dfs操作只有一个可变参数,因此需要一维DP就可以了,即这张表能够把所有暴力递归出现的情况都能包含在里面
  • 上面dfs过程中的n是不断变化的,因此下面我们将n替换成i
  • 我们可以将dfs循环拆分成if(!dfs(i - 1, memo) || !dfs(i - 2, memo) || !dfs(i - 3, memo)),改成dp的话,直接将其拿过来,改成dp数组就可以了,即if(!dp[i - 1] || !dp[i - 2] || !dp[i - 3])
  • 由上面转移过来的dp可得,dp[i]依赖于前面的结果,因此我们要从左到右进行遍历
  • 根据n <= 3,先后拿一定会赢。因此,直接拿过来确定好dp[1~3]=true
class Solution {
    public boolean canWinNim(int n) {
        return dpWay(n);
    }

    boolean dpWay(int n) {
        if(n <= 3) return true;
        Boolean[] dp = new Boolean[n + 1];
        dp[1] = true;
        dp[2] = true;
        dp[3] = true;
        for(int i = 4; i <= n; i++) {
            // 当拿了i石头,另一个人上一次拿的,只要有一个方案失败了,自己就是可以赢的
            dp[i] = !dp[i - 1] || !dp[i - 2] || !dp[i - 3];
        }
        return dp[n];
    }
}

4.找规律

我们可以这么理解,假设如果确保后手赢得话,无论如何都能配成4个,所以能够保证石头个数能被4整除,后手就能赢。而我们要求先手赢,取个反就可以了。

class Solution {
    public boolean canWinNim(int n) {
        // 假设如果是后手的话,无论如何都能配成4个,所以保证能够被4整除,后手就能赢,否则先手赢
        return (n % 4) != 0;
    }
}