In the "100 game," two players take turns adding, to a running total, any integer from 1..10. The player who first causes the running total to reach or exceed 100 wins.
What if we change the game so that players cannot re-use integers?
For example, two players might take turns drawing from a common pool of numbers of 1..15 without replacement until they reach a total >= 100.
Given an integer maxChoosableInteger
and another integer desiredTotal
, determine if the first player to move can force a win, assuming both players play optimally.
You can always assume that maxChoosableInteger
will not be larger than 20 and desiredTotal
will not be larger than 300.
Example
Input: maxChoosableInteger = 10 desiredTotal = 11 Output: false Explanation: No matter which integer the first player choose, the first player will lose. The first player can choose an integer from 1 up to 10. If the first player choose 1, the second player can only choose integers from 2 up to 10. The second player will win by choosing 10 and get a total = 11, which is >= desiredTotal. Same with other integers chosen by the first player, the second player will always win.
算法:
1.和前面的取石头游戏很像,仍可以从前面的状态转移方程得到可以借鉴的地方,就是我方赢的方法还是,试探取不同的可行的石子个数,如果任意一次有成功将对方置入必输状态,即可了。不过加入不能取重复的的限制瞬间变难,破解方法是DFS,去遍历每种取的顺序的可能。用一个boolean[]数组记录使用过的数字,遍历每种可能,暴力破解。
2.优化时间复杂度。单纯DFS会TLE,因为递归到底层开销很大,而且有很多相同的子状态被重复计算了。所以我们应该中途记录下某个状态对应的boolean结果,如果遍历到某个状态发现记录过直接返回结果即可。
问题:怎么定义一个状态?一开始以为要1.已用过的数字情况 2. total合在一起才是一个状态,后来发现单独一个1就足够了,因为在一个特定的问题中,maxI总是固定的,那如果你1固定了,maxI - 所有用过的数字就是2了,那2也固定了。所以2是包含在1里的。所以最后只要用一个Map<用过数字的情况,Boolean>来做memo即可。
问题:怎么表示用过数字的情况呢,这里又是一个优化,并不用boolean[] isUsed来表示,这样map要索引到的话每次要开一个新的boolean[],空间消耗。本题给了限制可使用数字不会超过20,利用它你可以使用int来取代boolean[]。也就是把integer上的每一位0还是1来当成boolean的结果。如TFFT用1001来表示。标记某一数用过没用过就转为位运算了。isUsed = isUsed ^ (1 << (数-1) )。一个位异或上1就是进行反转。
细节:
1. 这种DFS改变状态的标记isUsed每次改变一定要成对出现,改过,递归,改回来。本题也是要记住在for循环头尾要改,中途直接return要改(只是本题凑巧用int基本数据类型记录状态,如果是return的话本身就不被保留,可以不用改回去了)。
2. 判断胜利有两个条件,一个是这一步就赢了(可以取的数比界线高),一个是将来我肯定会赢了(我这几种可取的方法里有一种会让对手陷入必败之地)。不要漏了第一种。
3. 放入memo的是你调用这个函数的时候最开始的isUsed状态,而不是你改动后的,千万小心!
4. 位运算的时候尽量还是用括号括一下保证运算顺序,有几个优先级真的很难记,保险一点。
5. 边界条件有意思,一个是如果一开始可取的数就大于边界了,稳赢;一个是如果你们两个把所有数字都取完了都还没到边界,游戏没意义,你们都输
1.我的实现
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { // 边界条件有意思 if (maxChoosableInteger >= desiredTotal) { return true; } if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) { return false; } Map<Integer, Boolean> memo = new HashMap<>(); return helper(maxChoosableInteger, desiredTotal, 0, memo); } private boolean helper(int maxI, int total, int isUsed, Map<Integer, Boolean> memo) { if (memo.containsKey(isUsed)) { return memo.get(isUsed); } for (int i = 1; i <= maxI; i++) { //一定要把前面的计算结果扩起来,==优先级比&高 if (((isUsed >> (i - 1)) & 1) == 1) { continue; } // 某一位异或1就是反转这一位!这里反转就是改变标记用了没有的状态。 isUsed = isUsed ^ (1 << (i - 1)); // 注意 i >= total的时候已经可以判决是胜利了,其实相当于把出口化解在了这里。 if (i >= total || !helper(maxI, total - i, isUsed, memo)) { // 这里返回前按理说也要把使用状况改回来,但只不过java正好传引用参,回去的话本来就不会保留,所以就省略了。 // 这里千万注意memo里放的是你没改过的状态!!不是你改后的! isUsed = isUsed ^ (1 << (i - 1)); memo.put(isUsed, true); return true; } // 这里却还一定要记得把改变的状态恢复回来,因为for循环是函数内部,共享该变量,所以要改回来不能影响下一个变量。 isUsed = isUsed ^ (1 << (i - 1)); } memo.put(isUsed, false); return false; } }
2. 我的实现去掉注释方便阅读版
class Solution { public boolean canIWin(int maxChoosableInteger, int desiredTotal) { if (maxChoosableInteger >= desiredTotal) { return true; } if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) { return false; } Map<Integer, Boolean> memo = new HashMap<>(); return helper(maxChoosableInteger, desiredTotal, 0, memo); } private boolean helper(int maxI, int total, int isUsed, Map<Integer, Boolean> memo) { if (memo.containsKey(isUsed)) { return memo.get(isUsed); } int temp = isUsed; for (int i = 1; i <= maxI; i++) { if (((isUsed >> (i - 1)) & 1) == 1) { continue; } isUsed = isUsed ^ (1 << (i - 1)); if (i >= total || !helper(maxI, total - i, isUsed, memo)) { memo.put(temp, true); return true; } isUsed = isUsed ^ (1 << (i - 1)); } memo.put(temp, false); return false; } }
3.九章实现版
/** * 本参考程序来自九章算法,由 @九章算法 提供。版权所有,转发请注明出处。 * - 九章算法致力于帮助更多中国人找到好的工作,教师团队均来自硅谷和国内的一线大公司在职工程师。 * - 现有的面试培训课程包括:九章算法班,系统设计班,算法强化班,Java入门与基础算法班,Android 项目实战班, * - Big Data 项目实战班,算法面试高频题班, 动态规划专题班 * - 更多详情请见官方网站:http://www.jiuzhang.com/?source=code */ public class Solution { int[] dp; boolean[] used; public boolean canIWin(int maxChoosableInteger, int desiredTotal) { int sum = (1 + maxChoosableInteger) * maxChoosableInteger / 2; if(sum < desiredTotal) { //取完所有数,也达不到desiredTotal,无法赢得游戏 return false; } if(desiredTotal <= maxChoosableInteger) { //第一步就可以获得胜利 return true; } dp = new int[1 << maxChoosableInteger]; Arrays.fill(dp , -1); used = new boolean[maxChoosableInteger + 1]; return helper(desiredTotal); } public boolean helper(int desiredTotal){ if(desiredTotal <= 0) { return false; } int key = format(used); //把used数组转为十进制表示 if(dp[key] == -1){ for(int i = 1; i < used.length; i++){ //枚举未选择的数 if(!used[i]){ used[i] = true; if(!helper(desiredTotal - i)){ dp[key] = 1; used[i] = false; return true; } used[i] = false; } } dp[key] = 0; } return dp[key] == 1; } public int format(boolean[] used){ int num = 0; for(boolean b: used){ num <<= 1; if(b) { num |= 1; } } return num; } }