题目:
爱丽丝和鲍勃一起玩游戏,他们轮流行动。爱丽丝先手开局。
最初,黑板上有一个数字 N 。在每个玩家的回合,玩家需要执行以下操作:
选出任一 x,满足 0 < x < N 且 N % x == 0 。
用 N - x 替换黑板上的数字 N 。
如果玩家无法执行这些操作,就会输掉游戏。
只有在爱丽丝在游戏中取得胜利时才返回 True,否则返回 false。假设两个玩家都以最佳状态参与游戏。
示例 1:
输入:2
输出:true
解释:爱丽丝选择 1,鲍勃无法进行操作。
示例 2:
输入:3
输出:false
解释:爱丽丝选择 1,鲍勃也选择 1,然后爱丽丝无法进行操作。
提示:
1 <= N <= 1000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/divisor-game
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路:
这个题目标签为“简单”,但是又比一般的简单类型难一点。
不是简单的随机遍历。还要考虑:怎么让自己赢,以及如何让对方输。
所以根据要求N%X==0,在每次取数时就会有一定的规律:
如果N是奇数,那么只能随机抽取(1,N)之间的一个奇数X才可满足条件
如果N是偶数,那么直接抽取1即可。这样抽“我”输的风险最小。
Note:
整个抽取过程类似搜索,但是必须进行记忆化搜索,即记住中间结果以便搜索时的可能的使用,避免不必要的搜索,实现剪枝的效果。
在实现的时候,原本使用两个数组来记忆化:
①book数组来标识:数N时的博弈结果以前是否搜索过。book[N]=1表示搜索过。book[N]=0表示未搜索过。
②dp数组来标识:数N时的博弈结果。true则是爱丽丝获胜。否则失败。
后来进行精简化:将dp数组和book数组的功能整合到book数组中:
book[N] = 1时的含义不变
增加book[N] = 2,来表示数N是爱丽丝获胜。
实现(再次体会到C语言的高效性23333):
JAVA版:
package leetcode;
/*
USER:LQY
DATE:2020/7/24
TIME:8:04
*/
public class leetcode_1025 {
public static void main(String []args){
new leetcode_1025().divisorGame(3);
}
public boolean divisorGame(int N) {
int []book = new int[N+1];
boolean []dp = new boolean[N+1];
// 使用非精简版时需要如下初始化 。否则不需要初始化,保持全零即可。
dp[1] = false;
dp[2] = true;
book[1] = 1;
book[2] = 2;
boolean res = solve(N, 1, book, dp);
System.out.println(res);
return res;
}
// 非精简版
public boolean solve(int N, int flag, int []book, boolean []dp){
if(N <= 2) return dp[N] ^ (flag==1);
if(book[N] == 1) return dp[N];
book[N] = 1;
boolean res = false;
if(N%2 != 0){
for(int i = N-2;i >= 1;i -= 2){
if(N%i != 0) continue;
res = res || solve(N-i, -flag, book, dp);
}
}else{
res = res || solve(N-1, -flag, book, dp);
}
return dp[N] = res;
}
// 精简版。省去了dp数组,节省了内存空间
// 使用book数组来记忆化。空间换时间。
// book[N] == 0表示:数N时的博弈结果还未知。
// book[N] == 1表示在前面的博弈过程中,数N已经出现过,此时必定已经记录了数字为N时的结果。直接返回即可。
// 同时,book[N] == 1也表示数字为N时,爱丽丝失败。
// book[N] == 2表示:数字为N时,爱丽丝取胜。
public boolean solve(int N, int flag, int []book){
if(N <= 2) return (book[N]==2) ^ (flag==1);
if(book[N] == 1) return book[N]==2; //此时直接返回,因为已经记录了结果,不需要重复计算。
//没有记录过数N时的结果。
book[N] = 1; //因为当前就是要来计算数N时的博弈结果的,所以将book[N]标记为1(而不是2!!!)
boolean res = false; //数为N时的博弈结果暂存变量。初始肯定为false。
// 有两种取数策略:
// ①如果当前的N为奇数。那么“我”只能在(0,N)之间取出一个奇数x才能满足:N%x == 0 的条件。
if(N%2 != 0){
// 所以使用一个for循环遍历,类似查找。
for(int i = N-2;i >= 1;i -= 2){
if(N%i != 0) continue;
res = res || solve(N-i, -flag, book);
}
}
// ②如果当前的N为偶数,那么“我”可以直接就取出一个1就可以了
// 当然,如果想取出其他可以整除N的偶数也可,但是不符合博弈心理:“我”要想方设法赢。
// 所以,该题并不是简单的遍历。。。
else{
res = res || solve(N-1, -flag, book);
}
book[N] = res ? 2 : 1; //如果res为真,设book[N]=2来记录。
return res;
}
}
C语言版:
bool solve(int, int, int []);
bool divisorGame(int N){
if(N == 1) return false;
int book[N+1];
for(int i = 0;i <= N;i++) book[i] = 0;
// boolean []dp = new boolean[N+1];
return solve(N, 1, book);
}
bool solve(int N, int flag, int book[]){
if(N <= 2) return (book[N]==2) ^ (flag==1);
if(book[N] == 1) return book[N]==2;
book[N] = 1;
bool res = false;
if(N%2 != 0){
for(int i = N-2;i >= 1;i -= 2){
if(N%i != 0) continue;
res = res || solve(N-i, -flag, book);
}
}else{
res = res || solve(N-1, -flag, book);
}
book[N] = res ? 2 : 1;
return res;
}
Python版:
class Solution:
def divisorGame(self, N: int) -> bool:
if(N == 1):
return False
book = [0 for i in range(N+1)]
def solve(N: int, flag: int, book: List[int]) -> bool:
if(N <= 2):
return (book[N]==2) ^ (flag==1)
if(book[N] == 1):
return book[N]==2
book[N] = 1
res = False
if(N%2 != 0):
for i in range(N-2, 0, -2):
if(N%i != 0):
continue
res = res or solve(N-i, -flag, book)
else:
res = res or solve(N-1, -flag, book)
if(res):
book[N] = 2
return res
return solve(N, 1, book)