Dynamic Programming学习笔记 (24) - 除数博弈 (力扣# 1025)

168 阅读3分钟

本题出自力扣题库第1025题。题面大意如下:

甲乙两人一起玩游戏,两人轮流行动,玩家甲先手开局
开局时,黑板上有一个数字N (1 <= N <= 1000),在每个玩家的回合,玩家需要执行以下操作:
选择一个数x,要求0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
然后由对家继续以上的操作。
如果玩家无法执行这些操作,就会输掉游戏。
给定一个N,判断玩家甲是否能赢。
假设甲乙两人每一步都选择最佳策略。

实例:

N=2时,甲赢。因为甲首先挑1使N变成1,乙无法继续
N=3时,甲输。因为甲首先挑1使N变成2,然后乙挑1使N变成1,甲无法继续

题解:

这是DP应用中双人博弈类问题中较为简单的一个,该类问题的基本形式在于定义一个博弈的初始状态,一个用于改变博弈状态的规则,以及一个用于判断输赢结果的终结状态。参与博弈的双方依据规则,依次改变博弈状态,直到输赢结果出现。要求的则是对于一个给定的初始状态来判断博弈的结果,即谁输谁赢。对于博弈双方而言,因为他们都遵守同样的规则,所以此类问题往往是适用DP的递归问题。

对这个题面而言,给定的初始状态是数字N,改变其状态的规则是选择一个满足条件的x,然后当前状态改变为N-x,终结状态则是当找不到任何一个符合条件的x,博弈结果为输。

使用递归,我们可以很容易地实现这个判断逻辑,Java代码如下:

class Solution {
    Boolean[] dp;
    public boolean divisorGame(int n) {
        dp = new Boolean[n + 1];
        return helper(n);
    }

    private boolean helper(int n) {
        if (dp[n] != null) {
            return dp[n];
        }

        for (int x = 1; x < n; x ++) {
            if (n % x != 0) {
                continue;
            }

            if (!helper(n - x)) {
                return dp[n] = true;
            }
        }

        return dp[n] = false;
    }
}

以上代码使用一个递归函数helper来实现博弈状态的改变,以及输赢结果的判断,该函数的输入参数代表当前的博弈状态,返回值代表给定的博弈状态导致的输赢结果。其递归含义在于两个玩家轮流行动,甲行动时乙是下家,乙行动时甲是下家,无论谁在行动,其策略都是根据规则将当前状态转变为一个下家输的状态,如果这样的转变存在,那么当前玩家就赢,如果不存在,那么当前玩家就输,同时我们使用一个Boolean对象的DP数组来保存计算结果以避免重复计算。

在递归关系理清之后,我们也可以使用双重循环来依次计算DP数组中各个元素的值来实现相同的逻辑,Java代码如下:

class Solution {
    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) {
                    break;
                }

                if (! dp[i - j]) {
                    dp[i] = true;
                    break;
                }
            }
        }

        return dp[n];
    }
}

这里要注意的是外层循环的数组下标从2开始,而DP[1]中的值则是其初始值false,因为当N为1时,游戏的结果是先行的玩家输。

从数学角度出发,我们可以证明当给定的N是偶数时,先行的玩家总是赢,反之当N是奇数时, 先行的玩家总是输。