本题出自力扣题库第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是奇数时, 先行的玩家总是输。