464. 我能赢吗——记忆化搜索+状态压缩Java

目录

1.题目

2.思路

疑问1——为什么状态空间不是O(desiredTotal)呢?

疑问2——什么是正确的状态空间

疑问3——如何判断当前玩家获胜?(取决于dfs 的定义)【这个很关键】

疑问4——如何优化代码,提前结束?

2.代码

时间复杂度——O(2 ^ 20 * 20)

空间复杂度——O(2 ^ 20 )

4.结果


1.题目

464. 我能赢吗——记忆化搜索+状态压缩Java_第1张图片

2.思路

看见这个题,普通的dfs肯定不行,因为每次都会对最多20个数进行遍历,状态空间为m, 那么就是遍历m次个20,也就是20^m,这里的状态空间一开始想错了,以为就是累计和desiredTotal,也就是20^300次方,所以超时,所以需要用记忆化搜索的方法。(方法对,但是状态空间理解错了,属于误打误撞知道使用记忆化搜索)

dfs定义为题目所求:当前玩家先手能否获胜。

疑问1——为什么状态空间不是O(desiredTotal)呢?

举个例子,假如记忆数组是Mapmemo,最多存储desiredTotal个数。那么当前累计和为curSum, 使用一个Map记录哪些数已经被选中,可以用一个哈希表。一开始假如只能从1,2,3,4中选择数字,组成和为desiredTotal = 10.

错误代码示范:——可以通过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 )

疑问2——什么是正确的状态空间?

其实仔细想上面的错误代码,时间复杂度为O(desiredTotal ) * O(maxChoosableInteger) =300 * 20 =6000,这时间也太低了,大概率是不可能的。

正确的状态空间:O(2 ^ maxChoosableInteger),chooseNums用来记录哪些数字是否被选择。被选择的数字为1, 并且左移i位。这样相当于20个数字,每个数字都有选和不选,分别用1 和 0 表示。这样就可以解决上述两种情况的矛盾。

我们依旧用一个map存储。Mapmemo。curSum表示当前累计和,chooseNums记录哪些数字是否被选择。依然是从1,2,3,4选择,累计和位10.

第一种情况,选择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)

所以不会冲突。

疑问3——如何判断当前玩家获胜?(取决于dfs 的定义)【这个很关键】

一个情况是当前选择i + curSum >= desiredTotal

另一个情况是,下一个dfs()的值为false, 也就是下一个玩家不能赢,那么就代表当前玩家能赢

疑问4——如何优化代码,提前结束?

一种情况:等差数列求和判断选择所有的数字小于desiredTotal, 返回false

一种情况:如果可以选择的最大数字大于desiredTotal,返回true.(因为代表玩家可以第一次直接拿最大的数字即可。)

2.代码

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 * 20)

状态空间数O(2 ^ 20)

单次dfs()代码内一个循环O(20)

总的时间复杂度:O(2 ^ 20 * 20)

空间复杂度——O(2 ^ 20 )

记忆数组(或者成为记忆Map)的大小

4.结果

464. 我能赢吗——记忆化搜索+状态压缩Java_第2张图片

 

你可能感兴趣的:(LeetCode,leetcode,算法,记忆化搜索)