目录
1.题目
2.思路
疑问1——为什么状态空间不是O(desiredTotal)呢?
疑问2——什么是正确的状态空间
疑问3——如何判断当前玩家获胜?(取决于dfs 的定义)【这个很关键】
疑问4——如何优化代码,提前结束?
2.代码
时间复杂度——O(2 ^ 20 * 20)
空间复杂度——O(2 ^ 20 )
4.结果
看见这个题,普通的dfs肯定不行,因为每次都会对最多20个数进行遍历,状态空间为m, 那么就是遍历m次个20,也就是20^m,这里的状态空间一开始想错了,以为就是累计和desiredTotal,也就是20^300次方,所以超时,所以需要用记忆化搜索的方法。(方法对,但是状态空间理解错了,属于误打误撞知道使用记忆化搜索)
dfs定义为题目所求:当前玩家先手能否获胜。
举个例子,假如记忆数组是Map
错误代码示范:——可以通过50/57
class Solution {
// 定义记忆数组
Mapmemo = new HashMap();
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
// 提前判断
if(maxChoosableInteger >= desiredTotal)return true;
if(maxChoosableInteger * (maxChoosableInteger + 1) * 1.0 / 2 < desiredTotal)return false;
return dfs(maxChoosableInteger, desiredTotal, 0, new HashMap());
}
// 当前玩家为先手,能否赢
public boolean dfs(int maxChoosableInteger, int desiredTotal, int curSum, MapchooseNums){
// 边界
if(!memo.containsKey(curSum)){
boolean ans = false;
for(int i = 1; i <= maxChoosableInteger; i++){
// 是否使用过
if(chooseNums.getOrDefault(i, -1) == 1)continue;
// 中止
if(i + curSum >= desiredTotal){
ans = true;
break;
}
// int nextChooseNums = chooseNums + (1 << (i - 1));
chooseNums.put(i, 1);
boolean nextAns = dfs(maxChoosableInteger, desiredTotal, curSum + i, chooseNums);
chooseNums.put(i, -1); // 回溯
System.out.println(chooseNums + ".>>" + i +" " + curSum) ;
// 下一个玩家为false, 代表这个玩家就是赢
if(nextAns == false){
ans = true;
break;
}
}
memo.put(curSum, ans);
}
return memo.get(curSum);
}
}
其中有一种情况,有1,2,3数字被选中,curSum = 6。同时还会有别的情况,比如2, 4选中,curSum也等于6,这两个6的答案是可能不一样的。因为第一种情况可以选中下一个数字4,组成和为10,那么可以memo.put(6, true)。但是第二种情况,最大只能选中3,组不成和为10,所以只能memo.put(6, false)。所以矛盾,冲突了。换句话说,就是同一个surSum, 我们不能保证结果的唯一性,所以状态空间不能是O(desiredTotal )
其实仔细想上面的错误代码,时间复杂度为O(desiredTotal ) * O(maxChoosableInteger) =300 * 20 =6000,这时间也太低了,大概率是不可能的。
正确的状态空间:O(2 ^ maxChoosableInteger),chooseNums用来记录哪些数字是否被选择。被选择的数字为1, 并且左移i位。这样相当于20个数字,每个数字都有选和不选,分别用1 和 0 表示。这样就可以解决上述两种情况的矛盾。
我们依旧用一个map存储。Map
第一种情况,选择1,2,3,curSum = 6, chooseNums = (1 << 0) + (1<< 1) + (1 << 2)=7 (二进制位111) memo.put(7, true)
第二种情况,选择2,4,curSum = 6, chooseNums = (1<< 1) + (1 << 3)=10(二进制位1010) memo.put(10, true)
所以不会冲突。
一个情况是当前选择i + curSum >= desiredTotal
另一个情况是,下一个dfs()的值为false, 也就是下一个玩家不能赢,那么就代表当前玩家能赢
一种情况:等差数列求和判断选择所有的数字小于desiredTotal, 返回false
一种情况:如果可以选择的最大数字大于desiredTotal,返回true.(因为代表玩家可以第一次直接拿最大的数字即可。)
class Solution {
// 定义记忆数组
Mapmemo = new HashMap();
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
// 提前判断
if(maxChoosableInteger >= desiredTotal)return true;
if(maxChoosableInteger * (maxChoosableInteger + 1) * 1.0 / 2 < desiredTotal)return false;
return dfs(maxChoosableInteger, desiredTotal, 0, 0);
}
// 当前玩家为先手,能否赢
public boolean dfs(int maxChoosableInteger, int desiredTotal, int curSum, int chooseNums){
// 边界
if(!memo.containsKey(chooseNums)){
boolean ans = false;
for(int i = 1; i <= maxChoosableInteger; i++){
// 是否使用过
if(((chooseNums >> (i - 1)) & 1) == 1)continue;
// 中止
if(i + curSum >= desiredTotal){
ans = true;
break;
}
int nextChooseNums = chooseNums + (1 << (i - 1));
boolean nextAns = dfs(maxChoosableInteger, desiredTotal, curSum + i, nextChooseNums);
// 下一个玩家为false, 代表这个玩家就是赢
if(nextAns == false){
ans = true;
break;
}
}
memo.put(chooseNums, ans);
}
return memo.get(chooseNums);
}
}
状态空间数O(2 ^ 20)
单次dfs()代码内一个循环O(20)
总的时间复杂度:O(2 ^ 20 * 20)
记忆数组(或者成为记忆Map)的大小