Medium
这题真是,一开始看答案都看不懂, 听讲解也听不懂. 后来玩了一会儿回来看答案发现突然明白了. 基本上是recursion + memorization的方法.
用一个int[] state来表示现在每个ChoosableInteger是否被用到的状态,1表示是,2表示没有.比如maxChoosableInteger= 3, 那么state = {0,1,0,0}就表示1被用到,2,3都没有被用到,0在这里只是为了凑数使得下标和integer能一一对应. 那么每一次这个state表示的状态为了拿来做map的key, 我们就把它转化成一个string. 用到了Arrays.toString()
方法,比如上面的state转化成了"0100", 通过这种转换我们就可以记录下来每种状态下是否能赢的boolean值来做key. 这样就可以通过记忆化搜索防止重复计算.
那么要如何判断在某一状态下是否能赢呢?我认为本质上我们是模拟在当下的状态下,模拟接下来的两步来判断. 比如当前状态是state, 那么我现在可以去选一个还没有被叫到的数字i
, where state[i] = 0
. 如果我选了这个数字之后,和就已经到了或者超过desiredTotal, 那么我已经赢了, 也就是desiredTotal - i <= 0
时我赢;或是这一步我还不知道赢不赢,接下来轮到对手. 如果对手不赢,那肯定是我赢(因为游戏必然有一个人会赢),也就是!canIWin(desiredTotal - i, maxChoosableInteger, state, map)
这种情况我也会赢. 然后我们把会赢的结果true放到map的curtState这个key的value下, 记得要回溯将state[i]变回未被叫即state[i] = 0. 如果说这一步我不会赢,也要记得回溯.为什么必须要回溯呢?因为每一步你只能选一个数字叫,所以我们模拟的话每一次只能让state里面的某一个而且仅仅只有一个从0变到1.当我们检查过叫这个数字能不能赢之后,如果不能我们就会去检查叫其他数字能不能赢,这个前提是我们得把上一次尝试的那个数字变回没有被叫,才能保证整个尝试过程 每一次只选了一个数字. 如果在当下state下我尝试叫每一种没有被叫的数字都不能赢,说明这种 state下我必然输,则map.put(curtState, false)
.
class Solution {
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if (desiredTotal <= 0){
return true;
}
if ((1 + maxChoosableInteger)*maxChoosableInteger/2 < desiredTotal){
return false;
}
int[] state = new int[maxChoosableInteger + 1];
Map<String, Boolean> map = new HashMap<>();
return canIWin(desiredTotal, maxChoosableInteger, state, map);
}
private boolean canIWin(int desiredTotal, int maxChoosableInteger, int[] state, Map<String, Boolean> map){
String curtState = Arrays.toString(state);
if (!map.containsKey(curtState)){
for (int i = 1; i < state.length; i++){
if (state[i] == 0){
state[i] = 1;
if (desiredTotal - i <= 0 || !canIWin(desiredTotal - i, maxChoosableInteger, state, map)){
map.put(curtState, true);
state[i] = 0;
return true;
}
state[i] = 0;
}
}
map.put(curtState, false);
}
return map.get(curtState);
}
}