思路
- 首先判断特例:
- 如果最大能选择的数字maxChoosableInteger比期望的总数desiredTotal要大,先手稳赢,返回True
- 如果能选择的所有数字总和比期望的总数desiredTotal要小,一定到达不了desiredTotal,先手稳输,返回False
-
用二进制位来标记某个数是否已被选择,比如01表示1已被选择,10表示2已被选择,11表示1和2都被选择,100表示3被选择,以此类推,1 << i 表示 i + 1已被选择
-
n个数共有2n种被选择与否的状态,比如2个数有4种状态:00、01、10、11,用长度为 2n,也就是1 << maxChoosableInteger 的数组来存储这些状态是否已经出现过,以及这些状态对应是赢(True)还是输(False),即
Boolean[] map = new Boolean[1 << maxChoosableInteger];
为什么需要记录已经出现过的状态,因为我们在递归的过程中可能会有重复的状态出现,比如对于1 - 5 五个数的选择,先手选了3,后手选了5,剩下可选择的数为1,2,4;如果先手选了5,后手选了3,剩下可选择的数也是1,2,4。对于已经出现过的状态,不需要重复计算,而是直接返回之前已经计算过的结果dp[state]就可以了
-
递归函数的参数
public boolean canWin(int length, int nowTarget, int used, Boolean[] map)
:length:总长度(maxChoosableInteger ); nowTarget:现在需要凑出的数;used:当前遍历到想要使用的数;map:记录当前位置被使用后的成功与否情况 -
每次进入递归时
- 先判断一下当前位used数字是否已经出现过了,出现过就返回map[used],没有就继续往下计算
- 遍历能够被选择的所有整数(从1到maxChoosableInteger),当前数 i + 1 对应的状态cur为1 << i,如果当前数i + 1没有被选择(cur & used) == 0,进入内部
- 如果当前数i + 1比剩余要达到的目标nowTatget要大,说明选了这个数以后游戏就结束了,先手已赢,说明当前状态state可以让先手赢,map[used] = True,并返回True
- 如果当前先手选了i + 1之后还不能马上赢,但是下一步后手选数字的时候选输了(也就是
(!canWin(length, nowTarget - (i + 1), cur | used, map)) == true
,说明先手能赢,map[used] = True,并返回True - 如果遍历完了所有的整数,当前state还没有返回过True,说明这种状态就不能让先手赢,map[used] = False,并返回False
- 最后返回的就是第一层递归的结果,
canWin(maxChoosableInteger, desiredTotal, 0, map)
,也就是还没开始选择数字的时候(state为0时),能不能判断先手一定能赢
假设两位玩家游戏时都表现最佳:大概意思是每个人都会从剩余能选择的数字里选一个最后能让自己赢的数,递归里面的for循环就是在做这一件事,如果这个数不能让我赢,我就选下一个数看看能不能赢,只有当我选择任意的数都不能赢的时候,才会不甘心地返回一个False
代码实现
class Solution {
// 数组存储状态
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
// 总和都不大于desiredTotle,所以一定无法满足,返回false
if((maxChoosableInteger + 1) * maxChoosableInteger / 2 < desiredTotal) {return false;}
// 最大值比desiredTotal大,第一次选择必赢,所以返回true
if(maxChoosableInteger >= desiredTotal) {return true;}
// 创建Map集合来统计哪一个位置的数已经用过
Boolean[] map = new Boolean[1 << maxChoosableInteger];
//调用方法判断是否能赢
return canWin(maxChoosableInteger, desiredTotal, 0, map);
}
// 由于maxChoosableInteger 不会大于 20,所以可以使用一个int型的各个位标记是否使用(位运算)
// map[used]用于标记在使用used(二进制各个位真值代表某个元素是否已经使用,比如used = “1101”代表使用了1,3,4)情况本次挑选是否能赢
// 最大值(总长度), 现在的结果,当前位置,map集合标记
public boolean canWin(int length, int nowTarget, int used, Boolean[] map) {
if(map[used] != null){
return map[used];
}
// 探索当前可选的元素
for(int i = 0; i < length; ++i) {
// 第i为表示([1,2,3, maxChoosableInteger])选择i+1这个值
int cur = (1 << i);
// 判断这个值是否被使用过
if((cur & used) == 0) { // 表示没有使用过
// nowTarget <= i + 1是代表已经达到预期值
// nowTarget - (i + 1)表示选择了i + 1
// cur | used代表更新各个元素使用情况,使用i + 1,将used的第i位(从第到高)标记为1
// !canWin(length, total - (i + 1), cur | used, myMap)表示的是对方选择输了
if((nowTarget <= i + 1) || (!canWin(length, nowTarget - (i + 1), cur | used, map))) {
return map[used] = true;
}
}
}
return map[used] = false;
}
// Map集合存储状态
// public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
// // 总和都不大于desiredTotle,所以一定无法满足,返回false
// if((maxChoosableInteger + 1) * maxChoosableInteger / 2 < desiredTotal) {return false;}
// // 最大值比desiredTotal大,第一次选择必赢,所以返回true
// if(maxChoosableInteger >= desiredTotal) {return true;}
// // 创建Map集合来统计哪一个位置的数已经用过
// Map<Integer, Boolean> map = new HashMap<>();
// //调用方法判断是否能赢
// return canWin(maxChoosableInteger, desiredTotal, 0, map);
// }
// // 由于maxChoosableInteger 不会大于 20,所以可以使用一个int型的各个位标记是否使用(位运算)
// // map[used]用于标记在使用used(二进制各个位真值代表某个元素是否已经使用,比如used = “1101”代表使用了1,3,4)情况本次挑选是否能赢
// // 最大值(总长度), 现在的结果,当前位置,map集合标记
// public boolean canWin(int length, int nowTarget, int used, Map<Integer, Boolean> map) {
// if(map.containsKey(used)) {
// // 当前位置已经搜索过
// return map.get(used);
// }
// // 探索当前可选的元素
// for(int i = 0; i < length; ++i) {
// // 第i为表示([1,2,3, maxChoosableInteger])选择i+1这个值
// int cur = (1 << i);
// // 判断这个值是否被使用过
// if((cur & used) == 0) { // 表示没有使用过
// // nowTarget <= i + 1是代表已经达到预期值
// // nowTarget - (i + 1)表示选择了i + 1
// // cur | used代表更新各个元素使用情况,使用i + 1,将used的第i位(从第到高)标记为1
// // !canWin(length, total - (i + 1), cur | used, myMap)表示的是对方选择输了
// if((nowTarget <= i + 1) || (!canWin(length, nowTarget - (i + 1), cur | used, map))) {
// map.put(used, true);
// return true;
// }
// }
// }
// map.put(used, false);
// return false;
// }
}